Part II — Architecture and Methods · Chapter 7

Tactical Design: The Domain Model Building Blocks

~45 min read5 interactive widgets3 plates

In this chapter

  1. Model-driven design and the domain model pattern
  2. Entities
  3. Value objects
  4. Aggregates
  5. The three aggregate rules
  6. Aggregate granularity
  7. Domain events
  8. Event sourcing
  9. Services, factories, repositories, modules
  10. Transaction script versus domain model
  11. Check your understanding

1. Model-driven design and the domain model pattern

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.

The code should speak the ubiquitous language

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.

Domain model pattern

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.

Two constraints on the objects

The building blocks

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.

BOUNDED CONTEXT Client Application Services orchestrate only: NO business logic Factory hides complex construction AGGREGATE A root entity + entities + value objects AGGREGATE B business policies and logic modelled here Domain Services logic that is nobody entity job Repository persist / hydrate aggregates update the model THROUGH the aggregate root Database MODULES organise and encapsulate related concepts to keep a large domain model readable. All of these patterns share one theme: they put the business logic first.
Plate 7.1 — The tactical building blocks inside one bounded context. Note the two rules encoded in the arrows: application services orchestrate but hold no business logic, and the model is updated only through an aggregate root.

2. Entities

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 identityEquality is defined by the ID, not by the attribute values
Has attributesWhich may be entities or value objects — e.g. a Product with a retail price and a selling price
Can have state-changing methodsE.g. Change_Price_To: the state changes, the identity does not

3. Value objects

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:

Entities versus value objects

EntityValue object
EqualityDefined by identityDefined by value
MutabilityNot immutable: it has a state and typically the state changesImmutable
State-changing methodsCan have themCannot have them
RoleThe thing itselfValue objects describe an entity’s properties — e.g. a User has three value objects describing each instance: userId, name, password

4. Aggregates

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.

The key point

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.

Why: the large object graph problem

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.

Aggregate roots

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.

Properties

BEFORE — one large object graph Card Customer Address Postcode Order Line Product Price Size too large to remain consistent at all times too much information to load from the database each time AFTER — three aggregates Customer aggregate ROOT Card · Address · Postcode Order aggregate ROOT Line · Quantity · Price Product aggregate ROOT Color · Size · Price references are BY ID only, never the whole object graph THE ROOT IS THE ONLY DOOR: objects outside an aggregate cannot hold a reference to objects inside it; any change comes through the root, which encapsulates the data and exposes only behaviours; the aggregate is persisted and retrieved as a conceptual whole, in one atomic transaction. The aggregate is the TRANSACTIONAL AND CONSISTENCY BOUNDARY — and, in Part III, the reason a microservice can be decomposed out of a monolith at all.
Plate 7.2 — From an unbounded object graph to aggregates with roots. The vermilion arrows are identity references: the only legal way for one aggregate to know another.

5. The three aggregate rules

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.

The problem the rules solve: fuzzy boundaries

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.

Careful — the Sam and Mary scenario

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.

The rules

RuleStatementWhy
#1Reference 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 rootThis 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
#2Inter-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 restaurantIdIt 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
#3One transaction creates or updates one aggregate. A transaction can only create or update a single aggregateThis 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.

6. Aggregate granularity

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 smallMake them large
Because updates to each aggregate are serialized, more fine-grained aggregates increase the number of simultaneous requests the application can handle, improving scalabilityBecause 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.

7. Domain events

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.

Aggregates and domain events

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.

Why publish them

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.

Implementing them

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

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.

Identifying domain events

Generating, publishing and consuming

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.

8. Event sourcing

Event sourcing exploits domain events to create persistence. It is presented as a popular alternative to traditional snapshot-only persistence:

Event sourcing

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.

SNAPSHOT PERSISTENCE — one row, the present tense only stock_item | id=42 | quantity=1 | status=ADJUSTED the history is gone EVENT SOURCING — the series of events that led up to the state Domain Event Stock received Domain Event 1 item sold Domain Event 2 stock adjusted out Domain Event restocked append-only, immutable AGGREGATE state, rebuilt by replay TEMPORAL QUERIES: not only "what is the state?" but "what was the state at any time in the past?" Travel agent example: rebuild the catalogue as it was and re-run the searches users actually made.
Plate 7.3 — Event sourcing. The same idea returns in Chapter 12 as the event broker’s immutable, replayable log — the single source of truth for a whole platform.

9. Services, factories, repositories, modules

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.

10. Transaction script versus domain model

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.

Transaction script

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.

ProsCons
Very effective for simple business logicDoes 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

Domain model pattern

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 approachWhy
Easier to understand and maintainInstead 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 testEach class can and should be tested independently
Easier to extendIt 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.

For the exam

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.

Check your understanding

What is model-driven design, and what problem does it solve?

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.

Give Fowler’s definition of the domain model and the two constraints DDD adds to its objects.

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.

Distinguish entity and value object on four axes.

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.

What exactly is an aggregate, and what is guaranteed inside its boundary?

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.

What is an aggregate root and what does it forbid?

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.

Reconstruct the Sam and Mary scenario and state its moral.

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.

State the three aggregate rules.

#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.

Rule #2 looks like a design smell from an OO perspective. What does it buy?

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.

How is rule #3 reconciled with operations that must touch several aggregates?

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.

Should aggregates be large or small? Give the FTGO argument.

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).

What is a domain event, how is it named, and what does it carry?

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.

What is event enrichment and why do it?

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.

Define event sourcing and name its main benefit.

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 pasttemporal 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.

What is a domain service, and how does it differ from an application service?

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.

Why is a repository not just a DAO?

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.

When is a transaction script the right choice, and when does it break?

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.