DDD: Back to Basics¶
A few months ago, I wrote about what went wrong with Domain-Driven Design. In If You Apply DDD to DDD, You Won't Get DDD, I argued that the patterns became the goal, the terminology became a barrier, and the human work got buried under technical abstractions. That criticism stands. But criticism alone is incomplete.
Because not everything in DDD is noise. Strip away the pattern theater, the academic language, the certification industry, and you find a core that actually matters. Commands. Events. State. Aggregates. Ubiquitous Language. Bounded Contexts. These concepts are worth understanding, worth keeping, worth building on. This post is about that core.
The Triangle: Commands, Events, State¶
At the heart of every event-sourced system lies a fundamental relationship that deserves careful attention. Three concepts form a triangle, and understanding how they interact unlocks the entire paradigm.
Commands express intent. They are requests to change the system: BorrowBook, PlaceOrder, RegisterUser. A command can be accepted or rejected. It represents what someone wants to happen, not what has happened. When a customer clicks "Place Order," they are issuing a command. The system has not yet decided whether to accept it.
Events record facts. They describe what actually happened: BookBorrowed, OrderPlaced, UserRegistered. An event cannot be rejected because it already happened. Events are written in past tense for a reason. They represent reality, not wishes. Once an event is recorded, it becomes part of history.
State is derived. It is the current picture reconstructed from the history of events. State is a projection, a view, a snapshot. It tells you where things stand right now, but it is not the source of truth. The events are. You can always rebuild state by replaying events, but you cannot go the other way.
The triangle works like this: Commands produce Events. Events build State. State informs whether Commands can be accepted. A customer cannot borrow a book that is already borrowed. The state tells you it is unavailable. When the book is returned, the BookReturned event changes the state, and now the borrow command can succeed. This cycle is the heartbeat of event-sourced systems.
The distinction between intent (commands) and facts (events) is not academic. It changes how you think about errors, retries, and system behavior. A failed command is not a problem to hide. It is information. The customer wanted something the system could not allow. That is meaningful. That is business logic made explicit.
For those who want to explore the broader landscape of CQRS and Event Sourcing, cqrs.com provides a valuable resource for understanding how these concepts interplay. For a more detailed exploration of the triangle with code examples, see CQRS Without The Complexity. And for why events matter more than current state, see ... And Then the Wolf DELETED Grandma.
Why Boundaries Matter¶
Once you understand commands and events, the next question arises naturally: when you accept a command and write events, what guarantees do you need?
Imagine a system without boundaries. Any command can affect any data. Every write potentially conflicts with every other write. You need global coordination for everything. The system cannot scale. Reasoning about correctness becomes impossible. You cannot even tell which piece of code is responsible for which rules.
Boundaries solve this by defining a scope. This command affects this data. Within a boundary, you have strong guarantees: consistency, atomicity, the certainty that business rules hold. Across boundaries, you accept weaker guarantees: eventual consistency, coordination through events rather than locks. This tradeoff is not a compromise. It is a design choice that enables systems to scale while remaining correct where it matters.
This is what DDD calls an Aggregate. Not the pattern with the fancy name and the elaborate diagram. The fundamental idea: a boundary around data that changes together, where business rules must hold. When you borrow a book, the book's availability and the borrowing record change together. They form a boundary. The business rule "a book cannot be borrowed twice simultaneously" holds within that boundary.
The Language That Binds¶
Boundaries are not just technical. They are also linguistic.
One of DDD's most valuable ideas is deceptively simple: developers and domain experts should speak the same language. Not "developer language" translated into "business language" and back. The same language. The same words. The same meanings. Eric Evans called this the Ubiquitous Language, and despite the fancy name, the concept is profoundly practical.
When domain experts talk about "borrowing a book," your code should have a BorrowBook command and a BookBorrowed event. Not checkoutItem or updateLoanStatus. When they distinguish between "members" and "guests," your code should reflect that distinction. When they say "a reservation expires," your event should be ReservationExpired, not reservationStatus = 'inactive'. The words matter because they carry meaning. Every translation is an opportunity for misunderstanding. Every synonym is a potential bug.
This is not about pedantry. It is about alignment. When the code speaks the language of the domain, conversations between developers and domain experts become productive. You can point at code and discuss business rules. You can read an event stream and understand what happened in business terms. The gap between "what the system does" and "what the business needs" shrinks because both are expressed in the same vocabulary.
But here is where it gets architectural: the same word can mean different things in different contexts.
Consider "customer" in a large organization. To the sales team, a customer is a lead to nurture, with contact history and conversion probability. To the billing team, a customer is an account with payment terms and invoice history. To the support team, a customer is a ticket history and satisfaction score. These are not the same concept. They share a name, but they have different attributes, different behaviors, different rules. Forcing them into a single "Customer" model creates a monster that serves no one well.
This is what DDD calls a Bounded Context. A boundary within which a particular model and its language apply. Within the sales context, "customer" means one thing. Within the billing context, it means another. Both are valid. Both are precise within their scope. The boundary makes the difference explicit rather than hiding it behind a shared name that means different things to different people.
Bounded Contexts solve a problem that plagues large systems: the impossible quest for a single unified model. That quest fails because business domains are not unified. Different departments have different concerns, different vocabularies, different truths. A Bounded Context acknowledges this reality. It says: within this boundary, we have one consistent model and one consistent language. Across boundaries, we translate explicitly, because we know the concepts are different even if the names are similar.
The practical implication is liberating. You do not need to convince the entire organization to agree on what "customer" means. You need to be precise within your context and explicit about how your context relates to others. The sales context and the billing context can each have their own Customer model, optimized for their needs. When they need to communicate, they do so through well-defined interfaces, translating deliberately rather than assuming shared understanding.
Ubiquitous Language and Bounded Contexts are two sides of the same coin. The language gives you precision within a boundary. The boundary tells you where that precision applies. Together, they let you build systems that speak the language of the business without drowning in the complexity of making everyone agree on everything.
The Aggregate: Useful, But Not Sacred¶
With language and context boundaries understood, let us return to the technical boundaries we introduced earlier: aggregates.
Aggregates get some things right. They force you to think about consistency. They give you a unit of atomicity: everything inside an aggregate either succeeds or fails together. They provide clear ownership: this aggregate is responsible for these events, these rules, this data. In a world where code tends toward chaos, aggregates offer structure.
But aggregates also have problems. Real, practical problems that emerge in production systems.
Aggregates attract complexity. They grow. Logic gravitates toward them because "that is where the data is." A modest aggregate handling a few events becomes a God Object handling dozens. It becomes the dumping ground for everything related to the entity it represents. Splitting it later is painful.
Cross-aggregate operations are awkward. The moment you need to coordinate two aggregates, you reach for Sagas, Process Managers, Eventual Consistency patterns. A simple operation like transferring money between two accounts, something that happens in a single database transaction in traditional systems, becomes a complex workflow with compensation logic and failure handling. The cure feels worse than the disease.
The boundaries are static. Once you define an aggregate, changing it is hard. But business requirements evolve. What was one aggregate sometimes needs to split. What were two sometimes needs to merge. The code structure, optimized for yesterday's understanding, fights against today's needs.
For a deeper discussion of these tensions, see Kill Aggregate? An Interview on Dynamic Consistency Boundaries.
Dynamic Consistency Boundaries: Freedom and Risk¶
The frustrations with aggregates led to a different approach: Dynamic Consistency Boundaries, or DCBs. The idea, popularized by Sara Pellegrini, proposes that instead of fixed boundaries defined by aggregate classes, you define boundaries dynamically per operation. You query exactly the events you need. You lock exactly what you need. No more, no less.
The power is real. Cross-entity operations become trivial. Transferring money between accounts? Query both account streams, check both balances, write both events. No Saga needed. No eventual consistency for what is conceptually a single operation. The boundaries can evolve without restructuring your entire model. What was impossible becomes straightforward.
But there is risk too. Without explicit boundaries, where does logic live? If any command can touch any data, how do you organize code? How do you prevent the system from becoming a tangled mess where consistency rules are scattered everywhere?
DCBs work beautifully in small to medium systems with disciplined teams. In large systems with many developers, the lack of explicit structure can lead to low cohesion. The logic spreads. The consistency rules are everywhere and nowhere. Understanding "what affects what" becomes archaeology. You gain flexibility but may lose clarity.
The question is not whether DCBs are better than aggregates. The question is which tradeoffs fit your context. In the Rethinking CQRS: An Interview on OpenCQRS, Frank Scheffler discusses why they chose "No Aggregate" over "Kill Aggregate" and his concerns about DCB hype.
A Middle Ground: Multiple Events, Multiple Preconditions¶
EventSourcingDB offers a pragmatic middle ground. Not dogmatic. Not theoretical. Just useful.
The key insight: you can write multiple events to multiple subjects in a single atomic operation, with multiple preconditions.
Consider the classic example: transferring money between two accounts. Classically, this involves two aggregates. Account A and Account B. With traditional event sourcing, you need a Saga. Debit Account A. If that succeeds, credit Account B. If crediting fails, compensate by crediting Account A back. Simple business logic becomes infrastructure.
With EventSourcingDB, you can write both events, AmountDebited for account A and AmountCredited for account B, in a single transaction. You specify preconditions on both subjects: Account A must have sufficient balance, Account B must exist. If either precondition fails, nothing is written. The transfer either happens completely or not at all. No Saga. No compensation logic. No eventual consistency for a genuinely atomic business operation.
This is not DCB. You still have explicit subjects, similar to aggregates. You still think in terms of "this event belongs to this entity." The structure remains. The organization remains. You know where to look for the transfer logic.
This is not traditional aggregates either. You can coordinate across boundaries without the machinery of Sagas. Simple operations stay simple. Complex operations that genuinely require eventual consistency can still use it.
It is a middle ground: the clarity of explicit boundaries, without the rigidity of one-aggregate-per-transaction. For the technical details on how this works, see Preconditions.
When to Use What¶
Not every operation needs multiple subjects. Most operations are simple. A book is borrowed. An order is placed. A user updates their profile. These naturally affect only one entity. Use single-subject writes for these cases. Keep it simple. Do not reach for complexity when simplicity suffices.
Use multi-subject writes when a command genuinely spans entities and you need atomicity. Transfers between accounts. Reservations that affect both inventory and orders. Assignments that link a user to a resource. These are the cases where traditional aggregates push you toward Sagas, but the business sees them as a single operation. If the business considers it atomic, perhaps it should be atomic.
Consider DCBs when your domain genuinely has fluid boundaries. When the same event is relevant to multiple decision processes. When you need maximum flexibility and have the organizational discipline to manage it. But be aware of the tradeoffs. Freedom requires responsibility.
The goal is not to pick a camp. The goal is to pick the right tool for the job.
The Essence, Revisited¶
DDD's value is not in the patterns. It is in the questions the patterns try to answer.
What changes together? That is the question of boundaries, whether you call them aggregates, subjects, or consistency scopes.
What happened? That is the question of events. Facts. History. The immutable record of truth.
What do we want to happen? That is the question of commands. Intent. Requests. The expression of user and system desires.
What do we know right now? That is the question of state. Projections. Views. The derived picture built from events.
How do we talk about it? That is the question of language. The shared vocabulary that bridges the gap between business and code, precise within its context, explicit about its boundaries.
These questions matter regardless of whether you call your code an Aggregate, use DCBs, or something in between. They matter whether you use EventSourcingDB, another event store, or build your own. They are the essence that survives when the pattern theater closes.
The goal is not to implement DDD correctly. The goal is to build systems that are understandable, maintainable, and aligned with the business domain. DDD, at its best, is a set of tools for that goal. Not the goal itself. When the tools help, use them. When they hinder, set them aside. The questions remain either way.
If this perspective resonates, explore how EventSourcingDB handles boundaries and preconditions in the documentation. If you have questions about modeling your domain, whether with aggregates, DCBs, or something in between, reach out at hello@thenativeweb.io.
The patterns are tools. The questions are what matter. Start with the questions.