Different meta-models can be used to represent and implement a domain model — that is, models to describe the model. The one the course uses is the object-oriented meta-model, because it is a powerful conceptual framework: it captures not only entities and relationships but also behaviour, and it offers encapsulation and abstraction.
DDD promotes binding model and implementation, tightly relating the code to the underlying model — giving code meaning, and making the model relevant. The argument against keeping them apart is worth quoting: a pure analysis model not created with design in mind is typically quite impractical for those needs, and falls short of its primary goal of understanding the domain, because crucial discoveries always emerge during the design and implementation effort.
This is called model-driven design: it discards the dichotomy of analysis model and design, searching out a single model that serves both purposes. In DDD it is realised by the domain model pattern, which is part of the tactical design.
The domain model is an object model of the domain that incorporates both behaviour and data [Fowler, Patterns of Enterprise Application Architecture, p. 116].
At its worst, business logic can be very complex: rules and logic describe many different cases and slants of behaviour. It is this complexity that objects — as introduced with object-oriented programming, analysis and design — were designed to work with. A domain model creates a web of interconnected objects, where each object represents some meaningful individual, whether as large as a corporation or as small as a single line on an order form.
The object model is based on a set of DDD tactical patterns: entities, value objects, aggregates, domain events, domain services, factory, repository, modules. All of these patterns share a common theme: they put the business logic first.
Entities model any stateful entity requiring an explicit identification field to distinguish between the different instances of the entity, and featuring a state that can change over time.
The example is a user. The userId is itself a value object, and it can use any underlying data type that fits the business domain’s needs — a GUID, a number, a string, or a domain-specific value such as a Social Security number. The central requirement for the identification field is that it should be unique for each instance of the entity.
| An entity… | Detail |
|---|---|
| Has an identity | Equality is defined by the ID, not by the attribute values |
| Has attributes | Which may be entities or value objects — e.g. a Product with a retail price and a selling price |
| Can have state-changing methods | E.g. Change_Price_To: the state changes, the identity does not |
Value objects are concepts of the business domain that can be identified exclusively by their values, and thus do not require an explicit ID field. Examples: an object representing a colour given red/green/blue components; a point (x, y) of two-dimensional space; money.
Two consequences:
add method adding a vector v to a point p, obtaining a new point. For money, an Add method that returns a new Money instance.| Entity | Value object | |
|---|---|---|
| Equality | Defined by identity | Defined by value |
| Mutability | Not immutable: it has a state and typically the state changes | Immutable |
| State-changing methods | Can have them | Cannot have them |
| Role | The thing itself | Value objects describe an entity’s properties — e.g. a User has three value objects describing each instance: userId, name, password |
When modelling any domain model it is quite common to deal with composite or aggregate entities. Aggregates model entities that are actually a hierarchy of entities sharing a transactional boundary.
All of the data included in an aggregate’s boundary has to be strongly consistent to implement its business logic.
Evans’ own formulation of the consistency rule: invariants, which are consistency rules that must be maintained whenever data changes, will involve relationships between members of the aggregate. Any rule that spans aggregates will not be expected to be consistent at all times. Through event processing, batch processing or other update mechanisms, other dependencies can be resolved within some specified time — but the invariants applied within an aggregate will be enforced with the completion of each transaction.
The module shows a domain model in which Customer, Order, Line, Product, Address, CreditCard, Price, Quantity, Color and Size all reference each other. Two problems follow: the object graph is too large to remain consistent at all times, and it is too much information to load from the database each time. Cutting the graph into aggregates — a Customer aggregate, an Order aggregate, a Product aggregate — solves both.
Since an aggregate represents a hierarchy of entities, only one of them should be designated as the aggregate’s public interface: the aggregate root. It is the entry point of the aggregate.
Module 2.2 states the rules that make aggregates more than a modularisation device. In DDD, a key part of designing a domain model is identifying aggregates, their boundaries and their roots — the details of the aggregates’ internal structure is secondary. The benefit goes far beyond modularising a domain model, because aggregates must obey certain rules.
In traditional object-oriented design a domain model is a collection of classes and relationships, usually organised into packages. The explicit boundaries of each business object are missing. It does not specify, for example, which classes are part of the Order business object.
Two consequences. First, a conceptual fuzziness: performing an operation such as load or delete on an Order — what exactly does that mean? We would load or delete the Order object, but there is more to an Order than the Order object: order line items, the payment information, and so on. Second, and worse, the lack of explicit boundaries causes problems when updating a business object, because a typical business object has invariants — business rules that must be enforced at all times, such as a minimum order amount.
Two consumers, Sam and Mary, are working together on an order and simultaneously decide that it exceeds their budget. Sam reduces the quantity of samosas; Mary reduces the quantity of naan bread. Both retrieve the order and its line items from the database; both update a line item to reduce the cost. From each consumer’s perspective the order minimum is preserved.
Each consumer changes a line item using a sequence of two transactions: the first loads the order and its line items, and the UI verifies that the order minimum is satisfied; the second updates the line item quantity using an optimistic offline locking check that verifies the order line is unchanged since it was loaded. Sam reduces the total by $X and Mary by $Y. As a result the Order is no longer valid, even though the application verified the minimum after each consumer’s update.
Conclusion: directly updating part of a business object can result in the violation of the business rules.
| Rule | Statement | Why |
|---|---|---|
| #1 | Reference only the aggregate root. The root entity is the only part of an aggregate that can be referenced by classes outside of it; a client can only update an aggregate by invoking a method on the root | This rule ensures that the aggregate can enforce its invariant. A service uses a repository to load an aggregate and obtain a reference to its root, then updates it by invoking a method on the root |
| #2 | Inter-aggregate references must use primary keys. Aggregates reference each other by identity rather than by object references — an Order references its Consumer using a consumerId, and a Restaurant using a restaurantId | It looks like a “design smell” from a traditional OO modelling perspective, but: aggregates become loosely coupled; it avoids accidentally updating a different aggregate; if an aggregate belongs to another service it avoids object references that span services; and it simplifies persistence, since the aggregate is the unit of storage — easier to store in a NoSQL database such as MongoDB, no need for transparent lazy loading, and sharding aggregates to scale the database becomes relatively straightforward |
| #3 | One transaction creates or updates one aggregate. A transaction can only create or update a single aggregate | This constraint ensures that a transaction is contained within a service, and matches the limited transaction model of most NoSQL databases. It makes multi-aggregate operations more complicated — which is solved by the saga pattern, where each step of the saga creates or updates exactly one aggregate |
The module also notes the escape hatch and names it honestly: an alternative approach to maintaining consistency across multiple aggregates within a single service is “to cheat” and update multiple aggregates within one transaction.
A key decision when developing a domain model is how large to make each aggregate, and the two sides pull against each other.
| Make them small | Make them large |
|---|---|
| Because updates to each aggregate are serialized, more fine-grained aggregates increase the number of simultaneous requests the application can handle, improving scalability | Because an aggregate is the scope of a transaction, you may need a larger aggregate in order to make a particular update atomic |
| It improves the user experience, reducing the chance of two users attempting conflicting updates of the same aggregate |
The worked example: in FTGO, Order and Consumer are separate aggregates. An alternative design makes Order part of the Consumer aggregate. The benefit would be that the application can atomically update a Consumer and one or more of its Orders. But the drawbacks are serious:
Because of these issues, it is better to make aggregates as fine-grained as possible.
A domain event is a message describing a significant event that has occurred in the business domain. The goal of a domain event is to describe what has happened in the business domain and provide all the necessary data related to the event; it focuses on the when.
In tactical terms: something relevant at the domain level that has happened to an aggregate, usually representing a state change, represented by a class in the domain model. For the FTGO Order aggregate the domain events are Order Created, Order Cancelled, Order Shipped — state-changing events. An Order aggregate might, if there are interested consumers, publish one of these events each time it undergoes a state transition.
The classic illustration: in an e-commerce basket, every time a customer places an item in a basket it is important to update the recommended products displayed on the site. A domain event is raised with details of the basket; the event is subscribed to by the recommendation’s bounded context. Without using a domain event, you would need to explicitly couple the basket bounded context to the recommendation context. Domain events give a more natural flow of communication.
Other parties — users, other applications, or other components within the same application — are often interested in knowing about an aggregate’s state changes. The module lists the scenarios:
In every one of these scenarios the trigger for the notification is the state change of an aggregate.
A domain event is typically implemented by a class whose name is formed using a past-participle verb. It has properties that meaningfully convey the event, each either a primitive value or a value object, plus metadata such as the event ID and a timestamp — and possibly the identity of the user who made the change, which is useful for auditing.
In the FTGO code the pieces are: a DomainEvent marker interface identifying a class as a domain event; an OrderDomainEvent marker interface for events such as OrderCreated published by the Order aggregate; and a DomainEventEnvelope class containing event metadata and the event object, generic over the domain event type.
Event enrichment means enriching the event data model with further information that consumers may need. It simplifies event consumers, because they no longer need to request that data from the service that published the event, and it improves performance by avoiding further requests. In FTGO: consumers of OrderCreated events may need further information about the order; one option is to retrieve it from the Order Service, at the cost of a service request — enrichment avoids that.
Communicating using domain events is a form of asynchronous messaging: the events are published to a message broker.
The FTGO code shape: in KitchenService, the accept() method first invokes the TicketRepository to load the Ticket from the database; it then updates the Ticket by calling accept(); the Ticket aggregate’s accept() method returns a TicketAcceptedEvent to its caller; and the KitchenService then publishes the events returned by Ticket by calling publish() on the domainEventPublisher.
Event sourcing exploits domain events to create persistence. It is presented as a popular alternative to traditional snapshot-only persistence:
Instead of storing the state of an entity in a database, you store the series of events that led up to the state. The stream of events is then used to rebuild the state of an aggregate.
What it buys: storing all of the events increases the analytical capabilities of a business. Instead of just asking what the current state of an entity is, a business can ask what the state was at any time in the past. It allows powerful querying capabilities that revolve around time, known as temporal queries.
Being able to query the state of your domain model at any time in the past provides a competitive advantage, because you can correlate events that occurred in the real world with changes to the state of your domain model. The module’s example: online travel agents may want to investigate why the number of bookings had a massive dip in a certain month. With event sourcing they can rebuild the state of their catalogue and re-run the searches their users made, to understand why those users did not find a vacation they wanted.
A domain service object is useful to model some process or transformation in the domain which is not a natural responsibility of a specific entity or value object. It can involve multiple entities and value objects.
It is typically a stateless object implementing pure business policies and processes: for orchestrating calls to various components of the system to perform some calculation or analysis, and for coordinating the work of multiple aggregates.
Contrast with application services, which appear in the same diagram but do something different: application services orchestrate only — they contain no business logic. If you find business rules in an application service, they belong in an aggregate or in a domain service.
When the creation of an entity or a value object is sufficiently complex, we should delegate the construction to a factory object. A factory ensures that all invariants are met before the domain object is created.
Evans’ framing: every object-oriented language provides a mechanism for creating objects — constructors in Java and C++, instance creation class methods in Smalltalk — but there is a need for more abstract creation mechanisms that are decoupled from the other objects. A program element whose responsibility is the creation of other objects is called a factory. Just as the interface of an object encapsulates its implementation, allowing a client to use its behaviour without knowing how it works, a factory encapsulates the knowledge needed to create a complex object or aggregate, providing an interface that reflects the goals of the client and an abstract view of the created object.
The two key points: separate use from construction, and encapsulate complex entity and value object construction.
A repository provides functionality to retrieve and persist aggregates. A domain model needs a method for persisting and hydrating an aggregate from persistent memory such as a database.
The reason it is a pattern rather than just a DAO: because an aggregate is treated as an atomic unit, you should not be able to persist changes to an aggregate without persisting the entire aggregate. A repository abstracts the underlying persistence store from the model, allowing you to create a model without thinking about infrastructure concerns — it decouples the domain layer from database strategies and infrastructure code, and exposes the interface of an in-memory collection of aggregate roots.
A precise caveat from the source: for view rendering a repository is not required, and querying against a data store is the most efficient method for reporting needs. The repository is an infrastructure concern, so it is not always necessary to abstract it away. (This is the seed of the CQRS pattern in Chapter 11.)
Modules are used to decompose, organise and increase the readability of a large domain model — organising and encapsulating related concepts, entities and value objects, in order to simplify the understanding of larger domain models.
It is the same principle as Chapter 1’s modularity, applied one level down: inside a single bounded context, to the model itself.
Module 2.2 poses the tactical question directly: how do you organise the business logic of a service? There are two patterns, and the choice is a function of complexity.
Introduced in Fowler’s Patterns of Enterprise Application Architecture (2002), to be applied when we have simple business logic. The business logic is organised as separate independent procedures called transaction scripts: each script handles one request from the presentation tier — that is, from the inbound adapters. When implemented using objects, scripts are methods of classes that implement behaviour, separated from those that store state.
In microservices adopting the hexagonal architecture, scripts are usually located in service classes: a service class has one method for each request or system operation, and the method implements the business logic for that request, accessing the database using data access objects (DAOs); the data objects are pure data with little or no behaviour. In FTGO: the OrderService class has createOrder(), reviseOrder(), cancelOrder(), accessing the database through OrderDao.
| Pros | Cons |
|---|---|
| Very effective for simple business logic | Does not scale with complexity. As soon as the business logic becomes complex it is easy to end up with code that is critical to maintain and evolve — in the same way that a monolithic application has a habit of continually growing, transaction scripts have the same problem |
Adopting object-oriented design for the business logic: it consists of an object model — a network of relatively small classes that typically correspond directly to concepts from the problem domain. In such a design some classes have only either state or behaviour, but many contain both state and behaviour — a hallmark of a well-designed class.
As with the transaction script pattern, an OrderService class has a method for each request; but with the domain model pattern the service methods are usually simple: a service method almost always delegates to persistent domain objects, which contain the bulk of the business logic. It might, for example, load a domain object from the database and invoke one of its methods. In FTGO the Order class has both state and behaviour, and the state is private and can only be accessed indirectly via its methods.
| Benefit over the procedural approach | Why |
|---|---|
| Easier to understand and maintain | Instead of one big class that does everything, it consists of a number of small classes each with a small number of responsibilities, mirroring concepts of the domain |
| Easier to test | Each class can and should be tested independently |
| Easier to extend | It can use well-known design patterns such as Strategy and Template Method |
And the caveat that leads straight into Part III: a single application-wide domain model could lead to problems when used in a microservice architecture — hence the adoption of the DDD patterns, and in particular of aggregates and bounded contexts.
In a typical microservice, the bulk of the business logic consists of aggregates. The rest resides in the domain services and the sagas. Sagas orchestrate sequences of local transactions to enforce data consistency; services are the entry points into the business logic, invoked by inbound adapters; a service uses a repository to retrieve and save aggregates; and each repository is implemented by an outbound adapter that accesses the database. In FTGO’s Order Service: the Order aggregate, the OrderService service class, the OrderRepository, and one or more sagas — where a request local to the service simply updates the Order aggregate, while a request spanning multiple services makes OrderService create a saga.
It discards the dichotomy of analysis model and design, searching out a single model that serves both purposes. The problem it solves: a pure analysis model, not created with design in mind, is typically impractical and falls short of its own goal of understanding the domain, because crucial discoveries always emerge during the design and implementation effort. In DDD it is realised by the domain model pattern.
The domain model is an object model of the domain that incorporates both behaviour and data, creating a web of interconnected objects where each object represents some meaningful individual. The two constraints: the model must be devoid of any infrastructural or technological concerns — plain old objects, not relying on frameworks — and at the semantic level its objects must follow the terminology of the bounded context’s ubiquitous language.
Equality: an entity’s equality is defined by its identity, a value object’s by its values. Mutability: entities are not immutable — they have state that typically changes; value objects are immutable, because a change in one field semantically creates a new value. State-changing methods: entities can have them, value objects cannot. Role: value objects describe an entity’s properties — a User is described by the value objects userId, name, password. Note that value objects still model behaviour: methods that manipulate values and return new value objects.
An aggregate models entities that are a hierarchy of entities sharing a transactional boundary. Inside the boundary, all of the data has to be strongly consistent: the invariants applied within an aggregate are enforced with the completion of each transaction. Its invariants and internal objects can only be modified through its public interface by executing its commands, and its data fields are read-only for external components — so that all the business logic related to the aggregate resides within its boundaries. Any rule that spans aggregates is not expected to be consistent at all times.
The single entity designated as the aggregate’s public interface — the entry point of the aggregate. It forbids: no entity or value object outside the aggregate may hold a reference to an object within it; objects outside may only reference the aggregate root of another aggregate; every change must come through the root, which encapsulates the aggregate’s data and exposes only behaviours to change it.
Sam and Mary edit the same order simultaneously: Sam reduces the samosa quantity, Mary the naan quantity. Each uses two transactions — the first loads the order and its lines and the UI verifies the order minimum; the second updates a line with an optimistic offline locking check verifying that line is unchanged. Each individually preserves the minimum; together they reduce the total by $X + $Y and the Order becomes invalid. Moral: directly updating part of a business object can result in the violation of the business rules — which is why updates must be invoked on the aggregate root, which enforces the invariants, with concurrency handled by locking the root.
#1 Reference only the aggregate root — the root is the only part referencable from outside, and clients update the aggregate only by invoking a method on it; this is what lets the aggregate enforce its invariant. #2 Inter-aggregate references must use primary keys — aggregates reference each other by identity, e.g. consumerId rather than a Consumer object reference. #3 One transaction creates or updates one aggregate — which ensures a transaction is contained within a service and matches the limited transaction model of most NoSQL databases.
Four things: aggregates become loosely coupled; it avoids accidentally updating a different aggregate; if an aggregate belongs to another service it avoids object references spanning services; and it simplifies persistence, since the aggregate is the unit of storage — easier to store in a NoSQL database such as MongoDB, no need for transparent lazy loading and its associated problems, and sharding aggregates to scale the database becomes relatively straightforward.
With the saga pattern: a sequence of local transactions coordinated using messaging, where each step of the saga creates or updates exactly one aggregate. The module also names the pragmatic escape hatch for aggregates inside a single service — “to cheat” and update multiple aggregates within one transaction.
As fine-grained as possible. Small aggregates improve scalability, because updates to each aggregate are serialized, so finer granularity increases the number of simultaneous requests the application can handle, and they reduce the chance of two users conflicting. The counter-pressure is that the aggregate is the scope of a transaction, so atomicity may demand a larger one. FTGO: making Order part of the Consumer aggregate would allow atomic updates of a consumer and its orders, but it would reduce scalability (transactions on different orders of the same customer get serialized) and be an obstacle to decomposition (Order and Consumer logic must be collocated in one service).
A message describing a significant event that has occurred in the business domain, describing what has happened and providing all the necessary data related to the event — usually a state change of an aggregate. It is implemented as a class named with a past-participle verb, with properties that meaningfully convey the event, each a primitive value or a value object, plus metadata such as event ID and timestamp, and possibly the identity of the user who made the change, for auditing.
Enriching the event data model with further information that consumers may need. It simplifies consumers, who no longer need to request that data from the publishing service, and improves performance by avoiding those further requests. FTGO example: consumers of OrderCreated may need more information about the order, and the alternative — retrieving it from the Order Service — costs a service request.
Instead of storing the state of an entity in a database, you store the series of events that led up to the state; the stream of events is used to rebuild the aggregate’s state. The main benefit is analytical: instead of only asking what the current state is, the business can ask what the state was at any time in the past — temporal queries — which allows correlating real-world events with changes to the domain model, as in the travel agent rebuilding the catalogue and re-running past user searches.
A domain service models a process or transformation in the domain which is not a natural responsibility of a specific entity or value object, possibly involving several entities and value objects; it is typically a stateless object implementing pure business policies and processes, orchestrating calls to perform a calculation or coordinating the work of multiple aggregates. An application service, by contrast, orchestrates only and contains no business logic.
Because an aggregate is treated as an atomic unit: you should not be able to persist changes to part of an aggregate without persisting the entire aggregate. The repository abstracts the underlying persistence store from the model, letting you create a model without thinking about infrastructure concerns, decoupling the domain layer from database strategies, and exposing the interface of an in-memory collection of aggregate roots. Note the caveat: for view rendering a repository is not required, and querying the data store directly is the most efficient method for reporting needs.
It is the right choice when the business logic is simple: business logic organised as separate independent procedures, each handling one request from the inbound adapters, with data objects that are pure data with little or no behaviour. It breaks because it does not scale with complexity: as soon as the business logic becomes complex you end up with code that is critical to maintain and evolve — transaction scripts have the same habit of continual growth as a monolithic application.