Domain-Driven Design Distilled
by Vaughn Vernon
DDD for Me
The alternative to Good Design is Bad Design, not No Design. Design is inevitable, whether one wants it or not.
Questions about whether design is necessary or affordable are quite beside the point: design is inevitable. The alternative to good design is bad design, not no design at all. - Book Design: A Practical Introduction by Douglas Martin
Good Design can be terribly vague and broad when it comes to evaluating whether a software project has it. What is better is to mark up against Effective Design.
Effective Design translates to good-enough design that satisfies the needs of the business organization and gives it the edge that it needs to distinguish itself from its competition using Software. Effective Design also helps the business focus on the essential pieces of the software that differentiates them and guides them to the correct software model.
Most people make the mistake of thinking design is what it looks like. People think it’s this veneer—that the designers are handed this box and told, “Make it look good!” That’s not what we think design is. It’s not just what it looks like and feels like. Design is how it works. - Steve Jobs
Parts of Strategic Design:
- Bounded Context → Ubiquitous Language (with Domain Experts)
- Subdomains to deal with unbounded complexity of Legacy Systems
- Context Mapping between Bounded Contexts (both team relationships and technical mechanisms)
Tactical Design:
- DDD is about modeling your domain in the most explicit way possible.
- Aggregate Pattern to group entities and value objects together into the right-sized cluster
- Domain Events to share what has occurred within a model with other systems that may be interested in it (both within the Bounded Context as well as outside it)
Strategic Design with Bounded Contexts and the Ubiquitous Language
DDD is about modeling a domain expressed in Ubiquitous language in an explicitly Bounded Context.
- The word Bounded implies that the context has a boundary, within which each component of the software model has a specific meaning and it does specific things.
- The components are named after terms that occur in the Ubiquitous language.
When modeling a domain, you start off being conceptual, in the Problem Space. As the model matures, you move into Solution Space, which is the model represented in code.
You are in the Problem Space when you perform a high-level strategic analysis of your domain without worrying about actual implementation. These comprise most of the diagrams that pin down project goals and highlight risks. Context Maps work very well in problem space discussions.
You are in the Solution Space when you are implementing solutions to address aspects discovered in the Problem Space. When the Problem is strategically the most important to the organization, the Bounded Context that contains the software implementation is called the Core Domain.
The software model associated with the Core Domain is the most important for a business because it provides the differentiation factor to the organization. It typically addresses a major line of business, the one in which the organization excels. An organization should focus heavily on building and managing the Core Domain, at the cost of other supporting aspects of a business.
Bounded Contexts enclose the software implementation tackling a particular Problem, including architecture, source code, dependencies, and deployment scripts. They also contain code that supports integration with other Bounded Contexts.
The software model inside a Bounded Context reflects the common language used by Developers and Business teams alike, called the Ubiquitous language. The Ubiquitous Language is necessarily rigorous - strict, exact, stringent, and tight. Within a group, everybody understands what a term in the Ubiquitous language means precisely, including the term's constraints.
The team implementing the Bounded Context owns the terms in the Ubiquitous language. The terms are primarily represented in the software model's source code.
- A single team works on each Bounded Context. The same team can handle other Bounded Contexts, but a single Bounded Context should not be managed by multiple teams.
- Software artifacts of a Bounded Context - source code, database schema, deployment scripts - are kept separate, along with associated test cases.
When concepts from all over the software landscape are added to a domain model, the language of the model becomes blurred, resulting in confusing terminology. This confusion ultimately makes its way into software code, resulting in a Big Ball of Mud, with unrelated concepts spread across multiple modules, interconnected with conflicting concepts, and maintained by multiple teams.
Domain Experts and Business Drivers are good indicators of domain boundaries. Functional organization and expertise grouping in a company typically indicate different sub-domains. They may use the same term to mean different concepts in multiple domain, so concepts only make sense in a specific domain. It is simply accepting that there are different languages that each domain uses within itself. The domain acts as a namespace for the name of the domain element.
Bounded Context and Ubiquitous Language are two Strategic Design tools available to:
- Focus on the core of the domain and push out everything else
- Clearly articulate the meaning of each concept
The core concepts that remain in the context become part of the of Ubiquitous Language of the team that owns the Bounded Context. The Ubiquitous Language is developed collaboratively in a feedback loop between the team members.
Two groups come together to form the project team:
- Domain Experts: Focus on Business Concerns and think about how the business works - may or may not be the same as Product Owner. It's their mental model that lays the foundation for the Ubiquitous Language.
- Developers: Focus on Software Development. Should embrace the Ubiquitous Language inside their Bounded Context to codify software artifacts.
Developing a Ubiquitous Language:
- Don't limit your domain to nouns alone. Use the language expressed in scenarios that describe how the domain model should work, that will contain verbs, adverbs, constraints. These are conversations about how the domain model works - the design itself.
- Tools that help you document and communicate to the team are just that - they are tools. They help you develop the domain model, but are not the domain model itself. In the end, the code is the model and the model is the code.
- A scenario written in the form of a written statement can be transformed into a specification that can be used to validate your domain model, using techniques like BDD or Specification by Example. Such specifications can be maintained as Living Documentation, readable by the business user, but executable by build process to validate that the domain model is still intact.
- A Domain model is enhanced and kept consistent with the business over a long period of time - it is never really in Maintenance mode. If the business does not see value in the upkeep of a domain, it may not be central to the business.
A Bounded Context typically has these layers:
- Input Adapters: UI, REST Endpoints, and Message Listeners
- Application Services: Use Case Orchestration and Transaction Management
- Domain Model
- Output Adapters: Persistence, Message Dispatch
The Domain Model should remain free of technology. That's one of the reasons Transaction Management is outside it.
Strategic Design with Subdomains
Subdomain is a sub-part of a Business' Domain Model. Since most domain models can be too complex to reason about, it is easier to break a domain model into different problem spaces and work within them. A Subdomain is also a clear area of expertise, that provides a solution to the core area of the business, with the help of Domain Experts.
An ideal modeling composition would be one Subdomain per Bounded Context, and one Bounded Context per Subdomain. Other combinations are not optimal.
A Core Domain is a subdomain that forms the bread-and-butter of the Business. This domain is where the core expertise of the business lies, and it differentiates the business from its competitors. The business dedicates a significant amount of its software investment, collaboration efforts, time commitment, and experimentation of the business to the development of this domain.
A Supporting Subdomain is one that calls for custom development, but deals with an area that is supporting the core domain. By itself, a supporting subdomain does not differentiate the business, nor adds value. Only in conjunction with the core domain does it complete the value of the Business Product.
A Generic Subdomain is a solution for an area of the business that can be bought off the shelf, outsourced, or developed by a secondary team within the business.
Subdomains isolate problem spaces and facilitate discussions about areas of the business that one doesn't have control over but still need to deal as part of the Core business. These could be legacy systems, areas where the company has no expertise or capability, or domains that are too volatile and not well understood.
Monolithic applications typically have multiple Subdomains collapsed together into a single Bounded Context, creating a Big Ball of Mud. Thinking in the form of Subdomains helps limit the complexity of the problem space. By isolating Subdomains, we can identify the basic Ubiquitous Language terms and distinguish between valuable Subdomains versus those that are just playing a support role.
The goal of DDD is to have one Bounded Context per Subdomain. In other words, a Bounded Context should address just one subdomain and one alone. When not possible, code structures (like Modules) separates the supporting domain from the Core domain.
Strategic Design with Context Mapping
Context Map is an Integration between different Bounded Contexts in DDD. The Mapping indicates some relationship between the two bounded contexts, acting as a translation between the two Ubiquitous languages that exist in the two Bounded Contexts. The relationship could either be technical code-level integration or an inter-team dynamic.
A Context Map introduces an additional translation layer between two Bounded Contexts, either in code or in team dynamics. This translation is meaningful (because it bridges between the two Ubiquitous languages) but at the same time involves effort.
What is of interest is the kind of inter-team relationship or integration represented by the mapping between the Bounded Contexts. There are several kinds of Context Mappings, both team and technical, as well as a blend of both.
Types of Mapping:
- Partnership - Team-dynamic
- Two teams act as partners, each responsible for one Bounded Context. The partnership is to align the two teams to interdependent goals. They meet frequently, coordinate work schedules, and use Continuous Integration to keep their integrations intact.
- A thick mapping line represents such a relationship between the two teams.
- It is best to set a term limit on such a relationship because maintaining this synchronicity over the long term can be laborious.
- Shared Kernel - Team-dynamic
- Two (or more) teams share a small but common model.
- Teams mutually agree on what to share, and one team is usually responsible for coding, building, and testing what is shared.
- Customer-Supplier - Team-dynamic
- One Bounded Context (Upstream) acts as a Supplier of functionality, while the other acts as a Customer (Downstream). The supplier is in complete control of what to deliver and by when. The functionality to be developed is guided by the Customer, who needs to work with the Supplier for delivery.
- Conformist - Team-dynamic
- This relationship is similar to Customer-Supplier, except that the supplier does not support the specific needs of the downstream team. The downstream team does not make an effort to translate the Ubiquitous Language of the upstream model and uses the model as-is.
- Anticorruption Layer - Team-dynamic and Integration
- The Downstream team creates a defensive translation layer between its model and the model of the Upstream team. Integration through an ACL layer is a preferred approach when integrating between two models so that the concepts between the two models can be completely isolated and well-defined. It comes at the cost of increased maintenance, though.
- Open Host Service - Integration
- The Upstream Bounded Context defines a easily consumable protocol or interface for Downstream Bounded Contexts to integrate with and use. The API is well-documented and stable.
- Open Host Services are better than ACLs because of reduced burden on defining the intermediate layer. OHS is better for Conformist relationships too, because of the ease of integration provided by the Upstream Bounded Context - there is explicit acknowledgement of integration by Downstream consumers and efforts are taken to streamline the process.
- Published Language - Integration
- This mapping is similar to OHS except that a well-documented schema is published to enable consumption by any number of Downstream Bounded Contexts.
- The standard document can be in published in popular formats like JSON Schema, XML Schema, Protobuf, or Avro.
- Separate Ways - Team-dynamic
- Teams develop specialized solutions in their own Bounded Contexts eliminating the need for any integration or maintenance of common model.
- Big Ball of Mud - No Strategy
- Aggregates are contaminated because unnecessary connections and dependencies.
- Touching one of part of the model produces ripple-effects throughout the Bounded Context
- There is a dependence on Tribal Knowledge - and heroics - to keep the system running, creating concentrated, disconnected knowledge clusters.
Types of Integration:
- Remote Procedure Calls (RPC) with SOAP
- RPC defines a mechanism to easily access services from a system as a simple, local procedure or method invocation. The message travels over the network, so there is a risk of failure in message transfer.
- This type of integration works best if the Upstream Bounded Context provides a well-designed API and the Downstream Bounded Context employs the Anticorruption layer at the interface.
- RESTful HTTP
- REST protocol focuses on exchanging resources between Bounded Contexts, with four primary operations: POST, GET, PUT, and DELETE. Similar to RPC, the Upstream Bounded Context exposes a REST interface as an Open Host Service, with well-understood Published Language.
- This approach can fail for the same reasons as RPC but is far more robust because it is built on HTTP.
- Care should be taken to not expose resources that are in the shape of Aggregates in the Domain Model - this results in a Conformist relationship. Any change in the Aggregate Domain model will produce ripple effects across clients. Instead, resources should reflect the shape that the client needs.
- Messaging
- Bounded Contexts integrate with each other over messaging mediums, with the help of Domain Events published in themselves, or in other Bounded Contexts.
- Using messaging avoids the problems of temporal coupling that exist in RPC and REST integrations. It also ends up making the application robust because there is no expectation of immediate results.
- Case: REST clients can become asynchronous by subscribing to updates on an Atom feed, instead of needing to query for resources repeatedly.
- Case: Bounded Contexts that consume messages should not depend upon the Event Type (or class). They should only use the data parsed from the event payload to determine the next action.
- Case: While the Consuming Bounded Context typically receives Domain Events, it may sometimes trigger a Command Message on the Service Bounded Context to trigger an event. It still consumes the generated Domain Event to perform its processing.
- Case: To ensure robustness, the messaging mechanism should support At-Least-Once Delivery to ensure all messages will be received eventually. This also means that the subscribing Bounded Context is implemented as a Idempotent Receiver.
- At-Least-Once Delivery means that the messaging medium might deliver a message more than once even though the sender sent it only once. This is to handle cases of message loss, slow-reacting or down receivers, and receivers failing to acknowledge receipt.
- An Idempotent Receiver performs an operation in such a way that it produces the same result when it is performed multiple times. So it can safely handle a message that was delivered more than once.
- Case: Domain Event Payload size (Thin or Enriched)
- A Domain Event payload may be kept thin with the expectation that an interested Bounded Context will make a subsequent query requesting for the details it needs from the Service Bounded Context. This comes handy when security is a concern, or the payload is too big to ship to every consuming Bounded Context.
- The Domain Event can be enriched completely before sending on the wire. This allows more autonomy of dependent consumers.
Tactical Design with Aggregates
An Aggregate is composed of one or more Entities and Value Objects, with one Entity designated as the Aggregate Root. The Root Entity owns all the other elements within the Aggregate cluster. The name of the Root Entity is the Aggregate's conceptual name.
An Entity models an individual item in the domain. The Entity's state may or may not change over time, but its primary characteristic is its uniqueness - its individuality. Each Entity can be uniquely identified amongst other Entities.
A Value Object is a thing used to describe, quantify, or measure. It is usually an immutable Conceptual Whole. Value Objects don't have unique identities, and their data attributes determine their equivalence.
An Aggregate forms a transactional consistency boundary. All elements within an Aggregate must be consistent, according to business rules. Element data should preferably be consistent at all times, or at least by the time the controlling transaction is committed to the database.
Business rules define what parts of the system must be whole, complete, and consistent at the end of a Transaction.
Transactional Consistency does not necessarily translate to Atomic Database transactions. One can use patterns like EventSourcing and enforce consistency with databases with no support for atomic operations. So Transactional Consistency can be interpreted as a way to safely transition the state of the system such that all changes are valid and business rules compliant once completed.
The inverse of the Transaction Consistency rule is that if the persisted data is incomplete or invalid, then the business operation is deemed to be incorrect or to have failed.
A general rule with Aggregate design is that a transaction should only modify one Aggregate instance. Other Aggregates, where necessary, are updated at a later time to be eventually consistent.
Aggregate Rules of Thumb:
- Protect Business Invariants inside Aggregate boundaries
- Design Aggregates based on what must be consistent before a transaction commit.
- Design Small Aggregates
- Smaller Aggregates load quickly and take less memory, with a better chance at transactional success. Because they are small, Aggregates can also be easy to reason about, work on, and test.
- Each Aggregate should have a unique responsibility associated with it, in line with the Single Responsibility Principle.
- Reference other Aggregates by Identity Only
- Maintaining identities means that there is no easy way to obtain a direct object reference to an Aggregate, helping enforce the rule of only modifying one Aggregate at a time.
- Storing identifiers also means that any database technology - RDBMS, NoSQL, Graph - can be used to persist the Aggregates.
- Update Other Aggregates using Eventual Consistency
- Use Domain Events to propagate state change information to other Aggregates and Bounded Contexts.
Modeling Aggregates
- Avoid an Anemic Domain Model where Aggregates only contain data but no behavior. Also, prevent Business Logic from leaking into Application Services.
- Aggregate attributes should be set by internal methods, and never be exposed externally.
- The right abstractions in a Domain Model implementation tend to come out naturally when following the direction of the Ubiquitous Language.
- Steps to reach consistency boundary goals:
- Focus on designing smaller Aggregates first. Have just one Entity in each Aggregate, that serves as the Aggregate Root. The Entity should contain the unique identifiers that help identify and find the Aggregate, as well as other attributes/properties necessary to construct a valid Aggregate instance.
- Next, focus on satisfying all Business Invariants associated with an Aggregate. List all other Aggregates that need to be updated in reaction to a change in the Aggregate.
- Understand the allowed timeline for such updates in other Aggregates- immediately or within a certain period.
- Consider grouping entities that need to be updated immediately together.
- Update all other aggregates using eventually consistency mechanisms.
- Be aware of the bias to have real-time update to the entire system and fight it by providing:
- Examples of transaction failures due to concurrent udpates
- Memory overhead requirements for large aggregate clusters
- Eventual Consistency is business-driven. The Business can segregate parts of the domain that need immediate updates versus those that can make do with eventual updates. Aggregates enclose transactionally consistent domain elements, and Eventual Consistency mechanisms update state across Aggregates over time.
Tactical Design with Domain Events
Domain Event is a record of some business-significant occurrence in a Bounded Context. Domain Events not only serve as a historical record of events in a system, but also act as way to propagate a change across multiple Bounded Contexts to help bring the overall domain in sync.
A Domain Event's name is usually in the past tense, combining the name of the Aggregate in which the Event occurred and the verb indicating what happened. E.g. ProductCreated
, SprintScheduled
, BacklogItemPlanned
.
A Domain Event's name, together with its payload (properties, attributes), conveys what happened in the domain model in entirety. The Event's payload should contain only those attributes that triggered the Event. Enriching the Domain Event with additional information, while possible, should be avoided because the extra data makes it difficult to understand what exactly happened. Appending the full state of the Aggregate does not help either because of the same reason.
The modified Aggregate as well as the Domain Event have to be persisted together in the same transaction. In most DDD and CQRS systems, this means persisting into the Aggregate Entity's table and an Event Log in the same transaction. In an EventSourced application, only the Event needs to be saved because it represents the state of the Aggregate.
Interested Parties, both within the Bounded Context as well as outside, then receive the Domain Event over a messaging medium. The Domain Event may not arrive at the destination in the same order as it was published, so there is a need for a mechanism at the receiver's end to know whether the message has arrived out of sequence, and handle it appropriately.
Non-user events in the system can raise Domain Events as well. These events (usually time-based) are bubbled up directly as Domain Events and not as Commands because they represent a matter of fact and are not rejectable. Still, the Application may have to fire one or more Commands to act in response to Domain Events.
What is Event Sourcing?
- Event Sourcing is an architecture pattern where an Aggregate's state is preserved entirely in the form of events that happened with it over time. Events linked to an Aggregate instance, stored in the order they occurred, form the Aggregate's event stream.
- Domain Events, fired in response to commands, are stored sequentially in the event store and reconstituted by applying the events on an Aggregate instance one by one. Since the storage mechanism is write-only, Event Sourced systems tend to have high throughput with low latency and are highly scalable.
- High-performing Aggregates are cached in memory, eliminating the need to rebuild them every time on every use. Using the Actor model with actors as Aggregates can help in keeping the Aggregate's state cached.
- Snapshots can help retrieve Aggregates that are not in the cache without having to rebuild the Aggregate state from scratch. Each snapshot record stores the version number of the Aggregate along with other data, using which we can know if there are events that have occurred after snapshot generation.
Acceleration and Management Tools
Introducing EventStorming: Introducing EventStorming by Alberto Brandolini
Specification by Example: Specification by Example: How Successful Teams Deliver the Right Software: Adzic, Gojko: 9781617290084: Books - Amazon.ca
Reactive Messaging Patterns with the Actor Model: Reactive Messaging Patterns with the Actor Model: Applications and Integration in Scala and Akka: Vernon, Vaughn: 9780133846836: Books - Amazon.ca
User Story Mapping: User Story Mapping: Discover the Whole Story, Build the Right Product eBook: Patton, Jeff, Economy, Peter, Economy, Peter: Amazon.ca: Kindle Store
Impact Mapping: Impact Mapping: Making a big impact with software products and projects eBook: Adzic, Gojko: Amazon.ca: Books