skip to content

Your team enforces a strict rule that each transaction may modify at most one aggregate. A new Order aggregate is created by reserving stock across several Product aggregates and then persisting the Order. How should the factory/creation logic be structured so it respects the one-aggregate-per-transaction rule while still guaranteeing the Order is never created with stock it can't actually claim?

level: principalimportance: nice to knowfreq 20%

answer

  1. can't atomically touch Order + multiple Products in one txn
  2. reserve/hold per-Product, then create Order referencing reservation ids
  3. saga/compensating action to release on partial failure
  4. factory's guarantee narrows to 'local, already-confirmed data only'

basics

~20 s

Split it into steps: first reserve or hold the stock on each product (separate small transactions or a reservation step), then create the Order only if all reservations succeed, using its own transaction. Don't try to change the order and all the products at once - use short-lived holds and a way to release them if something fails partway through.

solid answer

~40 s

You can't atomically create the Order and decrement several Products in one transaction without breaking the one-aggregate-per-transaction rule, so the factory has to be redesigned around eventual consistency: each Product aggregate exposes a `reserve(quantity)` operation, invoked and committed one at a time (or via a saga/process manager), producing short-lived reservations rather than a permanent decrement. Only once all reservations succeed does the Order factory run, in its own transaction, creating an Order that references those reservation ids. If any reservation fails partway, a compensating action releases the reservations already taken. The Order's own local invariants are still checked synchronously by the factory; the cross-aggregate 'can I actually get this stock' guarantee becomes a multi-step, compensable process rather than a single atomic factory call.

go deeper

for a junior

Should sense that creating one thing while updating several other things at once is risky and something has to happen 'in steps,' without needing precise saga vocabulary.

for a middle

Should propose a reserve-then-create sequence and recognize partial failure needs handling somehow.

for a senior

Should name compensating actions explicitly and reservation expiry, and connect this to the one-aggregate-per-transaction constraint directly.

for a principal

Should name the saga/process-manager pattern, articulate exactly what the Order factory's guarantee narrows to, and reason about idempotency and concurrency control on the reserve operation itself.

## Why the rule and the factory pull against each other A one-aggregate-per-transaction rule exists to keep each transaction small, fast, and free of lock contention across unrelated aggregates - it's a deliberate constraint many DDD-influenced systems adopt specifically so that aggregates can scale independently and so that a transaction touching `Order` never blocks or gets blocked by a transaction touching an unrelated `Product`. The tension this creates for factories is direct: the textbook description of a factory **'guaranteeing invariants at construction time'** implicitly assumes the whole operation - checking and committing - happens as one atomic unit. Creating an `Order` that depends on successfully claiming stock from several different `Product` aggregates cannot be that: - doing it in one transaction means locking multiple aggregate rows together, exactly what the one-aggregate-per-transaction rule forbids - and even without that rule, at scale this creates severe lock contention on hot products ## The resolution: a short business process The resolution is to stop treating 'create the `Order`' as a single atomic operation and instead treat it as a short business process with its own intermediate states, sometimes implemented as a **saga** or **process manager**, sometimes as a simpler two-phase reserve/commit sequence. Concretely: 1. Each `Product` aggregate gets a `reserve(quantity)` command that, inside its own single-aggregate transaction, checks available stock and, if sufficient, creates a reservation record (a hold, not a permanent decrement) and returns a reservation id. 2. The orchestrating logic - which may live in an application service coordinating the process rather than inside any one aggregate's factory - calls `reserve` on each `Product` involved, one transaction at a time. 3. Only once all reservations succeed does it invoke the `Order` factory, in its own transaction, passing the collected reservation ids; the factory's job at that point is back to being purely local - validate that reservation ids are present and well-formed, compute the order total, assign an order number, and construct the aggregate. 4. If any individual reservation fails (stock actually insufficient, product deleted, etc.), a compensating step releases whatever reservations were already taken, and `Order` creation is aborted before the factory is ever called. ## What the guarantee narrows to This design still gives the `Order` factory a real invariant guarantee, but it's a guarantee about a narrower, purely local claim: an `Order` object returned by this factory always references reservation ids that were confirmed to exist. It does not, and structurally cannot, guarantee that the stock is still physically available right now, because time has passed since the reservations were taken and reservations themselves typically expire (a common pattern: a reservation is a hold valid for a fixed window, after which it's automatically released if not confirmed by a completed `Order` or payment). This is the DDD analogue of the local-vs-cross-aggregate invariant distinction taken to its logical multi-step conclusion: what used to be 'one factory call, one guarantee' becomes 'several local guarantees stitched together by a process that can fail partway and must know how to unwind.' ## The trade-off The trade-off is substantial added complexity. The team now needs to: - design and implement compensating actions (releasing reservations) - handle partial failure (three of four products reserved, the fourth is out of stock - now what happens to the three?) - decide on reservation expiry policy - often needs idempotency keys on the reserve operation so retries after a timeout don't double-reserve This is meaningfully harder to build and reason about than a single transactional factory call, and teams should only take it on when the one-aggregate-per-transaction constraint (or an equivalent distributed-systems reality, like the aggregates living in genuinely different services or databases) actually forces it - within a single database with no such rule, a pragmatic team might just wrap `Order` creation and `Product` stock decrements in one database transaction and accept the coupling, if the scale doesn't punish it. ## Failure modes in production Failure modes in production when this is built carelessly: - reservations that are taken but never released because the compensating action wasn't wired up for every failure path, silently locking stock that customers can't actually buy until an expiry job eventually cleans it up - race conditions between two orders both reserving the last unit of stock if the reserve operation itself isn't implemented with proper optimistic or pessimistic concurrency control inside the `Product` aggregate - orphaned Orders referencing reservation ids that later expired before payment completed, requiring an explicit recheck at a later step rather than assuming the Order's existence implies the stock is still held A well-known real-world pattern matching this shape is the **Saga pattern** as popularized in microservices architecture (and, in DDD circles, discussed extensively by Vaughn Vernon), where a long-running business process coordinates multiple aggregates - potentially across service boundaries - through a sequence of local transactions and compensating actions rather than a single distributed transaction, precisely because distributed two-phase commit across aggregate/service boundaries is avoided in favor of eventual consistency with explicit unwind logic.

  • What exactly does the Order factory's invariant guarantee cover in this reservation-based design, and what does it explicitly NOT cover?
    It covers only the local claim that the Order object it returns references reservation ids that were confirmed to exist at the moment they were checked - a purely local, synchronous check within the factory. It does not and cannot guarantee that the underlying stock is still physically reserved by the time the Order is actually used or paid for, since reservations can expire or the reserving process could fail after some but not all reservations succeeded.
  • What happens if three of four required product reservations succeed and the fourth fails because stock ran out?
    A compensating action needs to release the three reservations that did succeed, and Order creation is aborted before the Order factory is ever invoked - the process must be designed so partial success never leaves permanent stock holds or a half-created Order lying around. This is exactly the kind of unwind logic a saga/process manager is built to coordinate.

It's like booking a multi-leg flight itinerary across different airlines - no single system can lock all the seats on all three flights atomically, so a booking agent holds each seat one at a time, and only issues the final itinerary once all three holds succeed; if the third leg is sold out, the agent releases the first two holds rather than leaving you with two useless partial bookings.

saying these in an interview costs you the question

  • Proposes wrapping Order creation and multiple Product decrements in one transaction, ignoring the stated constraint
  • Doesn't mention compensating actions for partial failure
  • Assumes a reservation, once taken, is permanent with no expiry consideration
  • Can't explain why the factory's guarantee necessarily narrows once creation spans multiple aggregates/transactions

context