An Introduction to Domain-Driven Design

Tommy Cheese | · Reading time: 7 min

Overview of Domain-Driven Design

Domain-Driven Design (DDD) is a model-driven approach that captures domain knowledge in a domain model and uses that model to build more maintainable software.

DDD divides the design process into strategic and tactical design. Strategic design addresses domains, subdomains, and bounded contexts, while tactical design addresses entities, value objects, domain events, and related concepts. Their relationship is shown below:

img

Concepts in Strategic Design

Domain

A domain is the problem area that a system addresses. Product information management, for example, can be a system’s domain.

Subdomain

A domain can be divided into subdomains based on differences in the language used:

  • Core domain: the subdomain that determines a product’s competitive advantage. It contains the most important, distinctive, and central business functionality.
  • Generic subdomain: general-purpose functionality shared by multiple subdomains, such as authentication and authorization. These systems are not constrained by company-specific characteristics and need little customization.
  • Supporting subdomain: a subdomain containing neither core nor generic functionality. It is specific to the business but is not broadly reusable, such as a data dictionary for reference codes.

Bounded Context

A bounded context defines a boundary within which a particular model is used to solve a particular problem.

A bounded context is a subdomain or a collection of subdomains, Ensure that a bounded context supports a complete business process, Ensure that the domains involved in that process belong to the same bounded context. Bounded contexts provide a basis for dividing microservices, with each bounded context corresponding to a microservice.

Why do we need bounded contexts? Because context can make coding difficult. Consider an example:

In recruitment, “platform” might describe the school or organization someone comes from. In diving, it means the structure from which an athlete dives. In rail transport, it means the area where passengers board trains. Imagine that a diver, an HR specialist, and a train attendant meet without knowing one another’s professions. The HR specialist asks the diver, “Which platform are you from?”…

All three use the same word, “platform,” but without an agreed context they may interpret it differently. The solution is to separate the diving, recruitment, and transport domains and define terminology within each. Discussion within one domain then avoids as much ambiguity as possible. This is the agreement that a context establishes, and it explains why it is useful to define the domains involved in a business process within a bounded context: to reduce ambiguity.

Software design encounters the same problem. Suppose you design a large product covering diving, recruitment, and passenger transport without separating these services. You use a platform data structure for diving platforms, another platform for recruitment organizations, and yet another platform for railway platforms. When the three business areas inevitably meet, disaster follows: the screen fills with almost identically named variables that mean different things. What can we do?

By defining the precise meaning of a term within a particular domain, we can use that term freely within it. This is the essential role of a bounded context.

Concepts in Tactical Design

Value Objects and Entities

Let us begin with value objects and entities.

  • A value object is identified by its attribute values. If all the values inside two value objects are equal, we consider the objects equal. The attributes of a value object are therefore immutable.
  • An entity is a business object with a unique identity, state, and a lifecycle. Its attributes may change. Two entities with identical attributes are not necessarily the same entity; only identical identities, such as IDs, make them the same.

An example helps distinguish value objects from entities:

Suppose we have a white value object Color white{R:255, G:255, B:255} and a tire entity tire{Air:,Size:}:

  • Mutability: every attribute of white is immutable, because changing any of them means the object no longer represents white. A tire’s pressure, size, and other values may change, because it remains a tire even when these parameters change.
  • Equality: the immutability of a value object gives it its equality rule. Two white objects must have the same values, so two value objects are equal when all their internal values are equal. An entity does not have this property because its attributes are mutable.

Entities generally take one of four forms:

  • Bloodless model: the model contains only data definitions and getter/setter methods. Business and application logic reside in the service layer. Such a class is called a POJO in Java.
  • Anemic model: the model contains some business logic, but excludes logic that depends on the persistence layer. Persistence-dependent business logic resides in the service layer.
  • Rich domain model: the model contains all business logic, including logic that depends on the persistence layer.
  • Overloaded domain model: application logic unrelated to the business, such as authorization and transactions, is also placed in the domain model.

Repository (Repo)

A repository provides storage operations for entities. Its operations should be as low-level as possible, have short names, and remain limited in number. A MyBatis Mapper in Java can be viewed as one implementation of a repository.

Aggregates and Aggregate Roots

An aggregate is a larger unit of encapsulation that groups entities and value objects sharing a lifecycle and inseparable business meaning. Only the aggregate root exposes references outside the aggregate. Aggregates also express cohesion. Aggregate roots may call one another, and their names are generally nouns.

Aggregate roots provide encapsulation in the following ways:

  • All operations on an aggregate must go through its root; external code must not directly manipulate its elements. For example, purple paintwork + tires + a steel frame + … = a car. When driving, we do not independently operate the tires, steering wheel, or paintwork as separate objects. We operate the car as an aggregate root, indirectly affecting its other entities and value objects.
  • An aggregate defines a boundary within which every component must remain valid with respect to the business logic.
  • An aggregate must be operated on within an atomic transaction, or inconsistencies may arise. It is the unit of operation: retrieving it from the repository, modifying it, and putting it back form one atomic operation. For example, if a car aggregate contains wheel entities and a steel, aluminum, or carbon frame entity, removing a wheel for maintenance without reinstalling it cannot count as a completed operation.

Domain Events

Changes to an entity’s attributes produce domain events. A domain event represents something a domain expert considers important: a noteworthy occurrence within the domain. It usually indicates a change in a domain object’s state.Domain events carry messages and trigger further actions in a system, making them an important way to decouple domain models. We often use a message queue to deliver domain events, so that every subscribing subdomain can perform its own response.

A message bus is another implementation option 👋.

For example, a change in a tire entity’s pressure may emit a leaked event. This conveys that the tire has leaked and triggers other responses, such as changes in the power system or steering feel. Domain events are generally named in the past tense to indicate that they have already happened and cannot be undone.

Domain Service

Some actions in a domain appear not to belong to any object. They represent important domain behavior that cannot be ignored or simply attached to an entity or value object. When such behavior is identified, the recommended approach is to declare a service for it: a domain service.

A domain service is not the same as a service in microservices. Services and aggregate roots seem similar because both may operate on multiple entities, but their purposes differ: an aggregate root groups entities, while a service synchronizes the states of multiple entities—for example, adding an item entity to a list entity (such as delivering mail to an inbox). Service abstractions are therefore generally named with verbs.

Domain Modeling in DDD

The usual steps in DDD domain modeling are:

  1. Identify initial subdomains and bounded contexts from the requirements, along with the relationships between contexts.
  2. Analyze each context to identify entities and value objects, and decide which model style each entity needs: bloodless, anemic, rich, or overloaded.
  3. Associate entities and value objects, organize them into aggregates, and define aggregate boundaries and roots.
  4. Design repositories for aggregate roots and consider how entities and value objects should be created.
  5. Implement the domain model in the project, evaluate its suitability in practice, and use the results to identify shortcomings and refactor.

This is DDD’s two-stage design approach: strategic design first, followed by tactical design.

In practice, I suggest using bloodless entities, whose methods consist only of setters and getters, or anemic entities containing simple logic without database operations, such as attribute validation.

This is one approach to DDD modeling: top-down design. A bottom-up approach is also possible, starting with domain objects such as entities and value objects before identifying subdomains and bounded contexts.

(End of section)

comments powered by Disqus