
Two requests reserve the last unit. Each reads an available quantity of one and approves its purchase. Classes use business vocabulary, yet the software has sold a guarantee it cannot preserve. Calling a class an aggregate cannot coordinate concurrent writes.
I recommend Vaughn Vernon's Implementing Domain-Driven Design to developers connecting modeling with implementation. It becomes useful when you can name domain concepts but still need to decide which rules require a transaction, which work may happen later and how those choices reach code.
Addison-Wesley/Pearson's listing records the first edition as published on February 6, 2013, with a 2013 copyright. The book connects contexts and mapping to entities, values, events, aggregates and repositories. Java examples follow a SaaS application associated with agile project management.
Preserve that connection between strategy and detail while reading. A carefully implemented aggregate in the wrong context still protects an inappropriate business interpretation. Before discussing concurrency, establish what reserving means: holding stock for a period differs from recording purchase intent without a guarantee.
In Model True Invariants in Consistency Boundaries, Vernon describes an aggregate as a transactional consistency boundary guided by real invariants. This differs fundamentally from grouping objects because an ORM permits navigation between them.
For this review, consider hypothetical stock for one item at one warehouse. Policy accepts a reservation only when free quantity exists. Quantities are integers, requests must be positive, and the invariant is 0 <= reserved <= onHand. Availability derives from onHand - reserved.
Order and payment need not participate in that calculation to prevent reserving the last unit twice. Concurrent writes to the same balance do require coordination. The model can decide the rule in memory; persistence must preserve that decision against other operations. Neither part alone provides the complete guarantee.
This original exercise describes an expected outcome, not an execution or proof about a database. Initial state: item SKU-7, warehouse D-1, onHand=1, reserved=0, version=7. Commands A and B request one unit each for different orders, reading the same initial version.
Use optimistic concurrency in this design: saving requires the persisted version to match the version read. The winner increments it. Other strategies are possible but must preserve the same business rule. The winning order below is chosen only for illustration.
| Step | Expected outcome | Persisted state afterward |
|---|---|---|
A and B read version 7 | Both see one free unit | On hand 1, reserved 0, version 7 |
A saves requiring version 7 | Reservation R-1 accepted | On hand 1, reserved 1, version 8 |
B attempts to save requiring version 7 | Conflict; no balance write | On hand 1, reserved 1, version 8 |
B reloads and reevaluates | Business rejection: unavailable | On hand 1, reserved 1, version 8 |
A repeats its command after a timeout | Receives the result for R-1 | On hand 1, reserved 1, version 8 |
A technical conflict is not itself an out-of-stock rejection. A different change might have advanced the version without consuming availability. Reload and decide against current data. Blindly repeating a write approved against an old version recreates the risk we intended to remove.
Command A contains a stable identity, item, warehouse, order and quantity. Define the deduplication key and its scope, such as (warehouse, item, commandId). If the same key returns with a different order or quantity, report a content conflict. Reusing identifiers indiscriminately cannot provide idempotency.
In this design, first acceptance records the command result and updates the balance atomically. A unique constraint prevents concurrent records with the same key. The version check participates in the same commit unit. A failure in one step must not leave the others partially applied.
Checking a key, leaving the transaction and reserving afterward introduces another race. Recording only after responding also leaves a duplication window. The repository must expose saving and conflict conditions clearly. A generic save() hiding those choices makes the guarantee harder to assess.
A technical command record may occupy an auxiliary table without making every order part of the stock aggregate. That requires concrete storage and transaction decisions. Define retention too: when deduplication expires, the contract must state how long repeats remain recognizable, including after a reservation ends.
Free and paid products to turn ideas into a foundation you can actually ship.
13 products availableContinue exploring similar topics

Approach Eric Evans through a vocabulary conflict and discover where a model needs explicit boundaries.

Apply Michael Nygard's approach to timeouts, isolation and recovery through a controlled failure exercise.

Turn passive study into a learning project with direct practice, feedback and spaced retrieval.
A 47-point checklist to find bugs, security risks, and performance issues before launch.
Production-tested templates trusted by developers. Save weeks of setup on your next project.
Specific types for quantity and item identity can prevent invalid combinations before a balance changes. The benefit appears when a type rejects a negative quantity or distinguishes warehouse from order. Wrapping every field without restricting any operation merely adds ceremony.
Accepting R-1 can produce a ReservationAccepted fact containing reservation, item, warehouse, quantity and order reference. That fact has meaning in inventory. The order context decides how to translate it into its state without sharing the entire balance object.
In his 2011 essay Effective Aggregate Design, Part II, Vernon discusses relationships between aggregates and eventual consistency, including failure recovery. Applying that concern here, an in-memory event lost after saving cannot inform orders. An option such as an outbox must record publication intent alongside acceptance, then deliver with controlled repetition.
An outbox is an option for this exercise, not an automatic exactly-once guarantee. Consumers must recognize repeated messages and protect their own transitions. Sharing an event name cannot conjure a transaction across contexts.
After reservation, an order can temporarily await confirmation. The interface must distinguish that state from a completed purchase. A timeout alone cannot reveal whether inventory accepted or rejected the request. Allow lookup by command or reservation identity to recover a lost response.
Policy must also establish when R-1 expires and releases the unit. Expiry and consumption confirmation can race for the same reservation. Terminal states must prevent duplicate release or consumption after expiry without a new decision. Solving reservation creation alone leaves half its lifecycle unexplored.
Assign ownership for pending work, bounded retries and reconciliation. If payment succeeds after expiry, someone must decide between another reservation, a refund or support intervention. Eventual consistency creates intermediate processes; adopting it must buy enough benefit to sustain that work.
Putting inventory, orders, customers and payments into one aggregate makes unrelated changes compete within one boundary. Convenient ORM navigation can conceal excessive loading and unnecessary concurrency conflicts. Ask which invariant requires each element before including it.
The opposite extreme also fails: splitting every row into an aggregate can scatter the availability rule across independent writes. Small is a design direction, not a goal overriding the contract. A heavily contested final unit still requires arbitration with a well-chosen boundary.
A two-item cart raises another question: may the first item be reserved when the second is unavailable? If policy requires all or nothing, our single-balance example is insufficient. Negotiate partial reservations, compensation or a different transaction boundary. Adding a cart identifier to commands cannot create that guarantee. Record the decision before promising complete confirmation in the interface; otherwise a technically valid local reservation can still violate the customer's expectation for the overall purchase.
Avoid loading unlimited reservation history with a balance. Plan active records, auditing and queries while retaining release guarantees. This requires analysis of data access; an aggregate diagram cannot solve volume, indexing or contention for a popular item.
A unit test can check invalid quantities and insufficient availability. For the table above, also use actual storage: force two transactions to read version 7, allow the first commit and verify rejection of the second. Confirm that exactly one unit remains reserved.
Add simultaneous identical commands, repetition after timeout and key reuse with different content. Simulate failure before commit and between commit and publication. Check balance, recorded result and resumed delivery. Expectations must survive the database's actual isolation settings and constraints.
This review has executed none of those tests; it provides an exercise contract. A sequential in-memory simulation could explain the model but could not establish the storage's concurrency property. Keep that distinction visible in implementation documentation.
I recommend the book to developers who know Evans' approach and already work with persistence and transactions. It helps turn pattern names into verifiable questions. A continuing domain study provides context for connecting choices rather than reducing everything to unrelated small examples.
The edition requires adaptation to today's environment. Its Java examples should not become an obligation to reproduce hierarchies, frameworks or mappings. A domain model serves rules; an ORM serves persistence. When one silently dictates the other's boundary, modeling loses its justification.
For CRUD with straightforward rules, a direct transaction and clear validation may suffice. Postpone extensive adoption without a language or consistency problem to justify it. A team unable to operate replay and recovery should also assess asynchronous integration costs carefully.
Evans helps investigate meaning and context. Clean Architecture complements dependency separation. DDIA examines data and concurrency mechanisms. Layering cannot independently choose a business invariant; an aggregate name cannot replace storage guarantees.
Read strategy before transporting tactical patterns into a project. Then select an invariant, write states and commands like the table's, and examine its transaction. Discuss failure and recovery with the team operating the system. This is a study plan without a promise to redesign the entire application.
I recommend Vernon for bringing business commitments closer to implementation. You have used the book well when you can explain what remains consistent at commit, what may lag and how interrupted work resumes. Counting classes with DDD names offers a much less useful measure.