Walk through, step by step, what a command handler for something like `PlaceOrder` typically does from the moment it receives the command to the moment it finishes, and explain why a single handler is usually scoped to exactly one aggregate.
answer
- load-decide-apply-persist
- aggregate = transaction boundary
- one command, one aggregate, one transaction
- cross-aggregate effects via events, not direct calls
- handler defers to aggregate's own invariant logic
basics
~20 sIt loads the relevant object from storage, checks the request makes sense given that object's current state, changes the object, and saves it back, all as one all-or-nothing step touching just one thing, so it can't half-fail.
solid answer
~40 sA command handler receives and deserializes the command, loads the target aggregate from the repository by ID, invokes a method on the aggregate passing the command's data and letting the aggregate itself enforce its invariants, and the aggregate either raises resulting domain events or rejects the change. On success, the handler persists the new state, or appends the events, and commits, typically within a single database transaction. Scoping one handler to one aggregate keeps that transaction boundary small and consistent: the aggregate is the one thing whose invariants must be true immediately after the command, and a single-aggregate, single-transaction write avoids distributed transactions across services or tables. If a command needs to affect a second aggregate, that's done afterward, asynchronously, via the events the first aggregate raised, not inside the same handler call.
go deeper
Can describe the basic load, apply, save flow in their own words.
Explains why the whole flow happens inside one transaction and names the aggregate as the consistency boundary.
Can explain why cross-aggregate effects are pushed to async events or sagas instead of being handled inside one call, and the concurrency control involved, such as optimistic locking via a version number when loading the aggregate.
Can discuss handler design at scale - command dispatch/routing tables, how this pattern interacts with event sourcing where the handler appends events instead of saving snapshots, and the operational cost of getting aggregate boundaries wrong, such as hot aggregates and lock contention.
## The sequence a handler runs A command handler's job breaks down into a small, repeatable sequence of steps, and the discipline of keeping that sequence narrow is what makes command handling predictable at scale. 1. **First**, the handler receives the command, typically already deserialized and structurally validated by an earlier layer. 2. **Second**, it loads the target aggregate — the one consistency boundary the command is meant to affect — from its repository, using an ID carried on the command itself; in an event-sourced system this means replaying the aggregate's event stream, and in a state-stored system it means reading the current row or document. 3. **Third**, the handler invokes a method on the aggregate, passing along the command's data, and lets the aggregate's own code decide whether the change is legal given its current state — this is deliberate: the handler does not itself contain the business rule, it only orchestrates the call. 4. **Fourth**, the aggregate either raises one or more domain events describing what changed, or throws a domain exception if an invariant would be violated. 5. **Fifth**, on success, the handler persists the new state or appends the new events and commits, usually inside a single database transaction, and only after that commit does it publish any resulting events to the outside world. ## Why one handler, one aggregate The reason a handler is scoped to exactly one aggregate is that the aggregate is defined, in domain-driven design terms, as **the smallest unit within which invariants must hold true at all times**. If a handler tried to update two aggregates and enforce a rule that spans both of them in one atomic step, it would need a transaction spanning two consistency boundaries — which either means: - **a genuinely distributed transaction**, with all the coordination overhead and failure-mode complexity that implies, or - **a table-spanning transaction** that quietly couples two supposedly independent aggregates together at the storage level, defeating the purpose of having separate aggregates at all. Keeping one handler per aggregate keeps each transaction small, fast, and reasoned about entirely in terms of that one aggregate's own rules. ## The trade-off The trade-off is that any use case genuinely requiring two aggregates to change together can no longer be done atomically in the traditional ACID sense. Instead, the first aggregate's handler commits its own change and raises a domain event; a second, independent handler — potentially orchestrated by a saga or process manager for multi-step workflows — reacts to that event and updates the second aggregate in its own separate transaction. This buys you small, fast, independently scalable writes and clear invariant ownership, at the cost of **eventual consistency between aggregates**: for a window of time, possibly milliseconds, possibly longer under load or partial outage, the two aggregates can be observed in a state that would have been momentarily inconsistent if you'd been able to see both at once. ## Failure modes 1. **The most common production failure mode** is a team reaching for the atomic-across-two-aggregates shortcut anyway, usually under deadline pressure, by having one handler load and mutate two aggregates in the same transaction. This works fine in a monolith with one shared database until the two aggregates need independent scaling, independent deployment, or independent storage technology, at which point the coupling becomes a rewrite. 2. **A second common failure is putting business logic directly in the handler instead of the aggregate** — checking 'is this order over the credit limit' as an if-statement in the handler code rather than as a guarded aggregate method — which works until a second command path, say an admin override endpoint, needs to apply the same rule and either duplicates the check or, worse, forgets it. ## Placing an order, step by step A concrete example: in an order-management module, a `PlaceOrder` handler loads the `Order` aggregate by ID, calls `order.place(command)`, and the aggregate itself checks that the order has at least one line item and that the total is within any applicable limit before raising an `OrderPlaced` event; the handler then persists the new order row and publishes `OrderPlaced`. If placing the order should also decrement warehouse stock, that decrement happens in a separate `Inventory` aggregate's own handler, triggered by that `OrderPlaced` event rather than inside the same `PlaceOrder` call — so a warehouse outage that delays stock decrementing does not block or corrupt the order-placement transaction itself. ## Guarding against concurrent commands One more mechanical detail matters in practice: how the handler guards against two commands racing to load and modify the same aggregate concurrently, which is a distinct concern from the cross-aggregate coupling discussed above. Most implementations attach a **version number** to the aggregate, incrementing it on every successful write, and the handler's persist step includes that version in its update condition — an **optimistic-concurrency check**. If a second command loaded the same aggregate at the same starting version and tries to commit after the first one already advanced it, the second write fails outright rather than silently overwriting the first command's result, and the handler typically surfaces that as a retryable conflict rather than a business rejection, since simply reloading the now-current aggregate and reapplying the command will usually succeed. Without this guard, two concurrent commands against the same aggregate — two simultaneous `PlaceOrder` calls appending items to the same shopping cart, for instance — could each read stale state and each write back a result that silently clobbers the other's changes, a classic lost-update bug that only shows up under real concurrent load, rarely in single-threaded local testing.
- What does the handler do if the aggregate rejects the command, for example a business rule fails?It propagates the rejection back to the caller, typically as a thrown domain exception translated into an error response, or a Result-type failure, without persisting anything. No partial state change should ever be visible; the transaction should not commit.
- Why not just have one handler update two aggregates directly if the use case genuinely needs both to change?Doing so ties two aggregates into one atomic transaction, which usually means a distributed or cross-table transaction, defeating the purpose of small, independent aggregate boundaries. It also couples each aggregate's invariants to the other implicitly. The conventional fix is: the first aggregate's handler commits its own change and raises a domain event, and a separate handler or saga reacts to update the second aggregate, accepting eventual consistency between the two.
- Does the command handler contain business logic itself?No, ideally it's thin orchestration: load, call the aggregate method, persist, publish events. The actual business rules live inside the aggregate's own methods, not scattered in the handler, so the same invariant is enforced no matter which command path reaches the aggregate.
Think of a bank teller processing a withdrawal at one specific account. The teller pulls up exactly one account's ledger, checks the rules against that one ledger's current balance, updates it, and closes the transaction - they don't simultaneously update three other customers' accounts in the same motion. If the withdrawal needs to trigger something else, like a fraud alert to another department, that's a separate follow-up action, not part of the same at-the-counter transaction.
saying these in an interview costs you the question
- Puts business rule logic directly in the handler instead of inside the aggregate
- Has one handler updating two different aggregates in the same transaction as a matter of course
- Doesn't mention loading current state before applying the command
- Thinks the handler is responsible for publishing events to other bounded contexts synchronously, in-line, before returning