Domain-Driven Design — the tactical patterns

Published on
...

Domain-Driven Design (DDD) is an approach to software design that puts the business domain and its model at the centre of the code. It gives us patterns for expressing business concepts, rules, and object lifecycles clearly in software.

This article introduces the terminology and concepts in DDD, with code examples.

Quick reference

#PatternReach for it when
1LayersYou are starting anything that will outlive one sprint.
2EntitiesUsers need to tell one instance from another over time.
3Value objectsSwapping the object for an identical copy changes nothing.
4ServicesAn operation needs two or more objects, or an outside system.
5ModulesYou are naming the top-level folders.
6AggregatesA rule has to hold at the end of every transaction.
7FactoriesConstruction takes several steps or needs collaborators.
8RepositoriesA root must be found without following references.

1. Layers

In DDD, software is typically divided into four layers: UI, Application, Domain, and Infrastructure. What matters most is that dependencies flow in one direction and, above all, that the Domain remains isolated from UI and Application concerns.

UI → Application → Domain

The same rule, reached by two different callers

Rules that sit inside controllers or SQL are difficult to locate, and cannot be tested without a database and a request.

Layers — one rule, two callers
<?php

declare(strict_types=1);

/*
 * Layers — the same rule, reached by two different callers.
 *
 * The two halves are separate tabs, so each can be read on its own:
 * 1-before.php is the version that breaks, 2-after.php is the version
 * that holds. Run executes both, in that order.
 */

require __DIR__ . '/1-before.php';
echo "\n";
require __DIR__ . '/2-after.php';
ReadyRuns in your browser
Console

No output yet.

2. Entities

An object defined by its attributes rather than identity. It is usually immutable.

Entities are one of the fundamental building blocks in DDD. They give us a robust way to determine whether two objects represent the same thing, usually through a unique identity such as an ID. Entities are typically used for objects that have a lifecycle or history, where users need to distinguish one instance from another.

In practice, it can be useful to assign an entity’s UUID when the entity is created rather than waiting until it is persisted. Otherwise, the entity may temporarily have no identity, which can make equality checks and references to newly created entities more difficult or error-prone.

An identifier that arrives late is not an identity

Entities — identity before the INSERT
<?php

declare(strict_types=1);

/*
 * Entities — an identifier that arrives late is not an identity.
 *
 * The two halves are separate tabs, so each can be read on its own:
 * 1-before.php is the version that breaks, 2-after.php is the version
 * that holds. Run executes both, in that order.
 */

require __DIR__ . '/1-before.php';
echo "\n";
require __DIR__ . '/2-after.php';
ReadyRuns in your browser
Console

No output yet.

3. Value objects

An object defined only by its attributes. It has no id and does not change after construction.

Value Objects are another fundamental building block in DDD. They are typically used when replacing an object with another object that has the same attributes would have no effect on the system’s behaviour. What matters is the value itself, not the identity of the object.

For example, new Money(200) represents the same value whether it belongs to John or Josh. We do not care which specific Money instance we have, only that its value is 200.

Why a shared value has to be immutable

Value objects — sharing is only safe when immutable
<?php

declare(strict_types=1);

/*
 * Value objects — why a shared value has to be immutable.
 *
 * The two halves are separate tabs, so each can be read on its own:
 * 1-before.php is the version that breaks, 2-after.php is the version
 * that holds. Run executes both, in that order.
 */

require __DIR__ . '/1-before.php';
echo "\n";
require __DIR__ . '/2-after.php';
ReadyRuns in your browser
Console

No output yet.

4. Services

A stateless operation, named after an activity.

They are typically used when an important domain operation does not naturally belong to an Entity or Value Object. Instead of forcing that behaviour into an object where it does not fit, we can represent it as a Service.

For example, transferring money between two bank accounts may be better represented as a FundsTransferService, because the operation involves multiple Accounts and represents an important concept in the banking domain.

A good Domain Service should be stateless, use domain concepts in its interface, and represent an operation that is meaningful in the Ubiquitous Language.

The service that holds rules the entity should hold

Services — rules belong with the data
<?php

declare(strict_types=1);

/*
 * Services — the service that holds rules the entity should hold.
 *
 * The two halves are separate tabs, so each can be read on its own:
 * 1-before.php is the version that breaks, 2-after.php is the version
 * that holds. Run executes both, in that order.
 */

require __DIR__ . '/1-before.php';
echo "\n";
require __DIR__ . '/2-after.php';
ReadyRuns in your browser
Console

No output yet.

5. Modules

A cohesive group of domain concepts that should be understood together, usually reflected in code through modules, packages, or namespaces.

Modules are about how we organize the model and the code around meaningful business concepts. Since top-level folders are often one of the first things a new joiner sees, names taken from the business help describe what the system actually does, while names taken from technical patterns could describe almost any system.

Because renaming namespaces later can create large diffs, merge conflicts, and noisy history, it is better to get this structure right early. Group code by business concept, such as Tenant/ or Tenancy/, rather than by pattern, such as Entity/, Service/, or Dto/.

# Packaging by pattern — ask it "where are the rules about deposits?"
src/
├── Entity/
│ ├── Landlord.php
│ ├── Property.php
│ └── Tenancy.php
├── Repository/
│ └── TenancyRepository.php
├── Service/
│ ├── TenancyService.php
│ └── DepositService.php
└── Dto/
├── TenancyDto.php
└── TenancyMapper.php
# Packaging by concept — the same classes, regrouped
src/
└── Letting/
├── Booking/
│ ├── Tenancy.php
│ ├── Rent.php
│ ├── DepositCap.php
│ └── TenancyRepository.php
├── Property/
│ ├── Property.php
│ ├── Address.php
│ └── PropertyRepository.php
├── Customer/
│ ├── Landlord.php
│ └── Tenant.php
└── Infrastructure/
└── Persistence/
└── DoctrineTenancyRepository.php

6. Aggregates

A group of objects treated as one unit for changes, with a single root object and a defined boundary.

So far, we have introduced Entities and Value Objects as the fundamental building blocks in DDD. But once these objects start interacting with each other, we need some rules to define which objects belong together, how they can be accessed, and where consistency needs to be guaranteed at the end of a transaction. Aggregates give us a way to define that boundary.

An Aggregate is a group of Entities and Value Objects that we treat as a single unit when making changes. Each Aggregate has one Entity as its Aggregate Root, and objects outside the Aggregate should interact with the Aggregate through that root. The root is also responsible for making sure the Aggregate’s business rules and invariants remain valid when a transaction is completed.

For example, we might model a Tenancy as an Aggregate Root that contains related objects such as tenants, rent information, and deposit details. Instead of allowing other parts of the system to modify those objects directly, changes would go through the Tenancy, so it can make sure the whole tenancy remains in a valid state.

Two valid edits that produce one invalid result

Aggregates — the limit two people break together
<?php

declare(strict_types=1);

/*
 * Aggregates — two valid edits that produce one invalid result.
 *
 * The two halves are separate tabs, so each can be read on its own:
 * 1-before.php is the version that breaks, 2-after.php is the version
 * that holds. Run executes both, in that order.
 */

require __DIR__ . '/1-before.php';
echo "\n";
require __DIR__ . '/2-after.php';
ReadyRuns in your browser
Console

No output yet.

7. Factories

Code whose responsibility is constructing a complex object or a complete aggregate.

Factories are about how we create complex Entities or Aggregates without exposing all of their construction logic to the caller. When creating an object requires many steps, rules, or internal objects, putting all of that logic in the constructor can make the model harder to understand.

Instead, a Factory provides a clear way to create a complete and valid object. The caller only needs to provide the information required to create it, while the Factory takes care of the internal construction details.

We can use our previous example, Tenancy might require creating the tenants, rent details, deposit information, and other objects inside the Aggregate. Instead of making the caller construct each object individually, a TenancyFactory can create the whole Aggregate and return a valid Tenancy Aggregate Root.

Half-built objects, and the second door for loading

Factories — create and reconstitute are different doors
<?php

declare(strict_types=1);

/*
 * Factories — half-built objects, and the second door for loading.
 *
 * The two halves are separate tabs, so each can be read on its own:
 * 1-before.php is the version that breaks, 2-after.php is the version
 * that holds. Run executes both, in that order.
 */

require __DIR__ . '/1-before.php';
echo "\n";
require __DIR__ . '/2-after.php';
ReadyRuns in your browser
Console

No output yet.

8. Repositories

An object that behaves like an in-memory collection of every instance of one type.

Repositories are about how we find and retrieve existing Aggregates without letting persistence details leak into the domain. Instead of making the caller know how the data is stored, queried, or reconstructed, a Repository gives us a collection-like interface for accessing Aggregate Roots.

For example, if we need a Tenancy, the application can ask a TenancyRepository for it by ID. The Repository takes care of querying the database and rebuilding the Tenancy Aggregate, while the rest of the code only works with the domain object.

Repositories should normally be created for Aggregate Roots rather than every Entity inside an Aggregate. Internal objects should be accessed through their Aggregate Root.

The same answer, at two very different costs

Repositories — 10,000 objects, or none
<?php

declare(strict_types=1);

/*
 * Repositories — the same answer, at two very different costs.
 *
 * The two halves are separate tabs, so each can be read on its own:
 * 1-before.php is the version that breaks, 2-after.php is the version
 * that holds. Run executes both, in that order.
 */

require __DIR__ . '/1-before.php';
echo "\n";
require __DIR__ . '/2-after.php';
ReadyRuns in your browser
Console

No output yet.

Conclusion

In this article, we have focused on how the domain model is expressed, but domain objects also have a lifecycle, which brings us to Aggregates, Factories, and Repositories for defining transaction boundaries, creating valid objects, and finding them again later.

If you enjoyed this article, please click the buttons below to share it with more people. Your support means a lot to me as a writer.
Share on XShare on Threads

Subscribe for updates

Get a weekly recap with extra context, process notes, and the thinking behind the work.

Buy me a coffee