soda docs v17.0.1

Handler

Command, Query, and DomainEvent are all just objects stuffed with parameters; the ones who actually work are Handlers. Three message kinds map to three Handler kinds:

MessageHandler interfaceMethod signatureHow many per message
CommandCommandHandler<C, R>R handle(C command)exactly 1
QueryQueryHandler<Q, R>R handle(Q query)exactly 1
DomainEventEventHandler<T>void handle(T event)0 to n

Class names follow the message: CreateProducerCommand → CreateProducerCommandHandler, GetProducerQuery → GetProducerQueryHandler, ProducerEvent → ProducerEventHandler.

What a Handler calls down into

Handlers live in the application layer. They implement no business themselves — they only orchestrate. Downstream are two things:

HandlerCalls the aggregate rootCalls a RepositoryReturn value
CommandHandleryes — business rules live in the aggregate rootwrite repository (save / update / delete / operate)yes
QueryHandlerusually notread repositoryyes
EventHandlernever modifies the original aggregateonly when updating the read model; if it sends no new commands it may call nothingnone

In one line: the Handler executes, the aggregate root holds the rules, the Repository is the boundary of data access. Message → Handler → aggregate root / Repository — this chain is soda's skeleton.

What all three Handler kinds share

  • All ordinary Spring beans. Adding @Component is the entire registration — no subscribe, no registry, no XML.
  • Exactly one handle method. The parameter is the message itself; failures uniformly throw BaseException.
  • Which message it handles is decided by generics. At startup the bus injects collections like List<CommandHandler<?, ?>>, reads the message type out of the generics with GenericTypeResolver.resolveTypeArguments(...), and builds an index.
  • They live in the application layer: application/cqrs/handler/{command,query,event}.
  • All intercepted by the CqrsAroundHandler aspect. The aspect puts context into the ThreadLocal, records latency and in/out params, and cleans up in finally.
java
@Component
@RequiredArgsConstructor
public class CreateProducerCommandHandler
        implements CommandHandler<CreateProducerCommand, ProducerDTO> {   // ← generics decide "which command I handle"
    private final ProducerWriteRepository producerWriteRepository;

    @Override
    public ProducerDTO handle(CreateProducerCommand command) throws BaseException {
        ...
    }
}

How the bus finds a Handler

text
Command     ──→ CommandBus ──→ Map<Class, CommandHandler>       by exact class, 1
Query       ──→ QueryBus   ──→ Map<Class, QueryHandler>         by exact class, 1
DomainEvent ──→ EventBus   ──→ Map<Class, List<EventHandler>>   self + superclass + interfaces, 0..n

Commands and queries are looked up by the exact class of the message — register a Handler for GetProducerQuery, send a subclass of it, and it will not be found. Events differ: starting from the message class they walk up the superclass chain and implemented interfaces, so a Handler registered on DomainEvent receives every event.

CommandHandler: changing state

CommandHandler is the executor of write operations — a command gets exactly one:

java
@Component
@RequiredArgsConstructor
public class CreateProducerCommandHandler implements CommandHandler<CreateProducerCommand, ProducerDTO> {
    private final ProducerWriteRepository producerWriteRepository;

    @Override
    public ProducerDTO handle(CreateProducerCommand command) throws BaseException {
        commandValidate(command);                      // 1. validate
        Producer domain = ProducerAssembler.toDomain(command);  // 2. assemble the aggregate root
        domain.create(command.getName());              // 3. change state, suspend the event
        producerWriteRepository.save(domain);          // 4. persist via repository (the aspect publishes events here)
        return ProducerWebAssembler.toDTO(domain);
    }

    private void commandValidate(CreateProducerCommand command) throws BaseException {
        ParamChecker.notNull(command, "command");
        ParamChecker.notNull(command.getName(), "name");
    }
}
  • Has a return value. The R in CommandHandler<C, R> is exactly handle's return type.
  • Failures propagate up. A BaseException goes all the way up; if there is an outer transaction (your own @Transactional), it rolls back, and the suspended events are not taken by the aspect.
  • Business rules are not written here. The Handler only translates the message into aggregate root calls; the rules belong to the aggregate root.
  • Inject the interface, not the implementation. ProducerWriteRepository, not ProducerWriteRepositoryImpl.
Warning

Two Handlers registered for the same command will not raise an error — the later one silently overwrites the earlier. The startup log only shows two lines of Registered handler for command: ...; you cannot tell one got overwritten. One command type should have one Handler.

QueryHandler: reading state

Nearly the same shape as a CommandHandler; the difference is what it does:

java
@Component
@RequiredArgsConstructor
public class GetProducerQueryHandler implements QueryHandler<GetProducerQuery, Producer> {
    private final ProducerReadRepository producerReadRepository;

    @Override
    public Producer handle(GetProducerQuery query) throws BaseException {
        queryValidate(query);
        return producerReadRepository.getByUid(query.getUid());
    }
}
  • One query gets one Handler, again last registration wins.
  • Read-only. Do not call the write repository from here — the moment the read chain triggers a write, CQRS degrades back into an ordinary layered architecture.
  • Returning either a domain object or a DTO is fine, depending on which layer does the assembly. The two examples in Write a Query each use one.

EventHandler: after state has changed

An EventHandler does not "respond" — it does "follow-up". State has already been changed elsewhere; it receives the notification and handles what comes next:

java
@Component
@RequiredArgsConstructor
public class ProducerEventHandler implements EventHandler<ProducerEvent> {
    @Override
    public void handle(ProducerEvent event) throws BaseException {
        ParamChecker.notNull(event.getProducer(), "producer");
        // send messages, send notifications, update the read model...
    }
}
  • 0 to n; nobody handling it is not an error. Publish with no EventHandler receiving and it is silently skipped.
  • No return value. void handle(T event) — results can only show up as side effects.
  • Do not depend on execution order. Multiple Handlers for the same event are invoked in Bean injection order; the order is not part of the interface contract.
  • If one Handler on a class throws, the remaining Handlers for the same event do not run. SimpleEventBus calls them sequentially with no per-handler try/catch.
  • Failures do not roll back. Events are published after the transaction commits; RepositoryEventAspect can only log the exception. The Redis bus is the exception — it retries per max-retries, and final failures go to the dead-letter stream.
  • You can branch on eventType, or split into multiple event classes. The latter is clearer; see Domain Events.
  • You may send new commands from here. busFacade.sendCommand(...) continues the context — mind the recursion protection.

Common Handler pitfalls

SymptomCause
No handler registered for command: ...The Handler lacks @Component, or is outside the component scan
Command goes out and no Handler is found, though the class name is rightGenerics were written as a base class (e.g. CommandHandler<BaseCommand<X>, X>), so the index was built on the base class
EventHandler never firesThe event class's generics are wrong; or bus-type is set to spring, which creates no EventBus bean at all
Changed a Handler but behavior didn't changeA second Handler for the same command/query is overwriting it
No Registered handler log at startup@Component didn't take effect, or the package is not on the scan path

Names end with Handler, but they are not message Handlers

A few other classes in a generated project end with Handler; they have nothing to do with the three message Handler kinds above:

ClassWhat it isWhat it doesWhere it comes from
ControllerAroundHandlerWeb layer aspectReads Soda-Request-Id / user name from request headers, fills them into the command, writes MDCthe generated project's adapter layer
CqrsAroundHandlerCQRS aspectIntercepts the handle of all three Handler kinds and CommandBus.send; moves context, logs, prevents recursionthe soda-core framework
GlobalExceptionHandler@RestControllerAdviceMaps BaseException to Result.fail(code, message) as the fallbackthe generated project's adapter layer

They handle no commands, queries, or events — they only do cross-cutting work. When you see a Handler suffix, don't rush to file it under the three message Handler kinds.

Next steps