Domain-Driven Design: part 3
One year ago, we already made a quick presentation of
Domain-Driven Design, in the context of our mORMot
After one year of real-world application of those patterns, it is now time to give more light to DDD.
Let's continue with part 3, which will define Domain-Driven Design patterns and principles - this will be the main article of the whole serie!
Ubiquitous Language is where DDD begins.
DDD expects the domain model to be expressed via a shared language, and used by all team members to connect their activities with the software. Those terms should be used in speech, writing, and any presentation or diagram.
In the real outside world, i.e. for the other 10th kind of people how do not know about binary, domain experts use company- or industry-standard terminology.
As developers, we have to understand this vocabulary and not only use it when speaking with domain experts but also see the same terminology reflected in our code. If the terms "class code" or "rate sets" or "exposure" are frequently used in conversation, we shall find corresponding class names in the code. In DDD, it is critical that developers use the business language in code consciously and as a disciplined rule. As a consequence, browsing the code should lead into a clear comprehension of the business model.
Domain experts will be the guard keepers of the consistency of this language, and its proper definition. Even if the terms are expected to be consistent, they are not to be written in stone, especially during the initial phase of software development. As soon as one domain activity cannot be expressed using the existing set of concepts, the model needs to be extended. Removing ambiguities and inconsistencies is a need, and will, very often, resolve several not-yet-identified software issues.
Value Objects and Entities
For the definition of your objects or internal data structures (what good programmers care about), you are encouraged to make a difference between several kind of objects. Following DDD, model-level representation are, generally speaking, rich on behavior, therefore also of several families/species of objects.
Let us list the most high-level definitions of objects involved to define our DDD model:
- Value Objects contain attributes (value, size) but no conceptual identity - e.g. money bills, or seats in a Rock concert, as they are interchangeable;
- Entity objects are not defined by their attributes (values), but by their thread of continuity, signified by an identity - e.g. persons, or seats in most planes, as each one is unique and identified.
The main difference between Value Objects and Entities is that instances of the second type are tied to one reality, which evolves in the time, therefore creating a thread of continuity.
Value objects are immutable by definition, so should be handled as
read-only. In other words, they are incapable of change once they are
Why is it important that they be immutable? With Value objects, you're seeking side-effect-free functions, yet another concept borrowed by DDD to functional languages (and not available in most OOP languages, until latest concurrent object definition like in Rust or Immutable Collections introduced in C#/.NET 4.5). When you add $10 to $20, are you changing $20? No, you are creating a new money descriptor of $30. A similar behavior should be visible at code level.
Entities will very likely have an
ID field, able to
identify a given reality, and model the so-called thread of continuity
of this identity. But this
ID is an implementation detail, only
used at Persistence Layer level: at the Domain Layer level,
you should not access Entities individually, but via a special
Entity bounded to a specific context, called Aggregate Root
(see next paragraph).
When we define some objects, we should focus on making the implicit become
explicit. For instance, if we have to store a phone number, we won't
use a plain
string type for it, but we should create a dedicated
Value object type, making explicit all the behavior of its associated
reality. Then we will be free to combine all types into explicit grouped types,
Aggregates are a particular case of Entities, defined as collection of objects (nested Values and/or Entities) that are grouped together by a root Entity, otherwise known as an Aggregate Root, which scope has been defined by a given execution context - see "Composition" above.
Typically, Aggregates are persisted in a database, and guarantee the consistency of changes by isolating its members from external objects (i.e. you can link to an aggregate via its ID, but you can not directly access to its internal objects). See the Shared nothing architecture (or sharding) which sounds like an Aggregate-Oriented Database
In practice, Aggregates may be the only kind of objects which will be persisted at the Application layer, before calling the domain methods: even if each nested Entity may have its own persistence method (e.g. one RDBMS table per Entity), Aggregates may be the unique access point to retrieve or update a given state. It will ensure so-called Persistence Ignorance, meaning that domain should remain uncoupled to any low-level storage implementation detail.
DDD services may just permit remote access to Aggregates methods, where the domain logic will be defined and isolated.
Factory and Repository patterns
DDD then favors some patterns to use those objects efficiently.
The Factory pattern is used to create object instances. In
strongly-typed OOP (like in Delphi, Java or C#), this pattern is in fact its
constructor method and associated
class type definition,
which will define a fixed set of properties and methods at compilation time
which you can add methods and properties at runtime).
The Factory pattern can also be used to create
interface instances. Main
benefit is that alternative implementations may be easily interchanged. Such
abstraction helps testing but
also introduces interface-based services.
Repository pattern is used to save and dispense each Aggregate
It matches the "Layer Supertype" pattern (see above), e.g. via our mORMot
TSQLRest classes and
their Client-Server ORM features, or via dedicated repository classes - saving
data is indeed a concern orthogonal to the model itself. DDD architects claim
that persistence is infrastructure, not domain. You may benefit in defining
your own repository interface, if the standard ORM / CRUD operations are not
DTO and Events
In addition to these domain-level objects, some cross-cutting types may appear, especially at Application layer and Presentation layer:
- Data Transfer Objects (DTO) are transmission objects, which purpose is to not send your domain across the wire (i.e. separate your layers, following the Anti-Corruption Layer pattern). It encourages you to create gatekeepers that work to prevent non-domain concepts from leaking into your model.
- Commands and Events are some kind of DTO, since they communicate data about an event and they themselves encapsulate no behavior - in mORMot, we try to let the framework do all the plumbing, letting those types be implemented via interfaces, avoiding the need to define them by hand.
Those kind of objects are needed to isolate the domain from the outer world.
But if your domain is properly defined, most of your Value Objects may
be used with no translation, so could be used as DTO classes. Even
Entities may be transmitted directly, since their methods should not
refer to nothing but their internal properties, so may be of some usefulness
outside the domain itself.
Only the Aggregates should better be isolated and stay at the Application layer, given access to its methods and nested objects via proper high-level remote Services.
Aggregate roots (and sometimes Entities), with all their
methods, often end up as state machines, and the behavior matches
In the domain, since Aggregate roots are the only kind of entities to which your software may hold a reference, they tend to be the main access point of any process. It could be handy to publish their methods as stateless Services, isolated at Application layer level.
Domain services pattern is used to model primary operations.
Domain Services give you a tool for modeling processes that do not have an identity or life-cycle in your domain, that is, that are not linked to one aggregate root, perhaps none, or several. In this terminology, services are not tied to a particular person, place, or thing in my application, but tend to embody processes. They tend to be named after verbs or business activities that domain experts introduce into the so-called Ubiquitous Language. If you follow the interface segregation principle, your domain services should be exposed as dedicated client-oriented methods. Do not leak your domain! In DDD, you develop your Application layer services directly from the needs of your client applications, letting the Domain layer focus on the business logic.
Unit Of Work can be used to maintain a list of objects affected by
a business transaction and coordinates the writing out of changes and the
resolution of concurrency problems.
In short, it implements transactional process at Domain level, and may be implemented either at service or ORM level. It features so-called Persistence Ignorance, meaning that your domain code may not be tied to a particular persistence implementation, but "hydrate" Aggregate roots class instances as abstractly as possible.
Clean Uncoupled Architecture
If you follow properly the DDD patterns, your classic n-Tier architecture will evolve into a so-called Clean Architecture or Hexagonal architecture.
Even if physically, this kind of architecture may still look like a classic layered design (with presentation on the top, business logic in the middle and a database at the bottom - and in this case we speak of N-Layered Domain-Oriented Architecture), DDD tries to isolate the Domain Model from any dependency, including technical details.
As a consequence, the logical architecture of any DDD solution should appear as such:
That kind of architecture is not designed in layers any more, but more like an Onion.
At the core of the bulb - sorry, of the system, you have the Domain
It implements all Value Objects and Entity Objects, including their state and behavior, and associated unit tests.
Around this core, you find Domain Services which add some more
behavior to the inner model.
Typically, you will find here abstract interfaces that provides persistence (Aggregates saving and retrieving via the Repository pattern), let Domain objects properties and methods be defined (via the Factory pattern), or access to third-party services (for service composition in a SOA world, or e.g. to send a notification email).
Then Application Services will define the workflows of all
Even if the core Domain is to be as stable as possible, this outer layer is what will change more often, depending on the applications consuming the Domain Services. Typically, workflows will consist in deshydrating some Aggregates via the Repository interface, then call the Domain logic (via its objects methods, or for primary operations with wider Domain services), call any external service, and validate ("commit", following Unit-Of-Work or transactional terms) objects modifications.
Out on the edges you see User Interface, Infrastructure (including e.g.
database persistence), and Tests. This outer layer is separated from the other
three internal layers, which are sometimes called Application
This is where all technical particularities will be concentrated, e.g. where RDBMS / SQL / ORM mapping will be defined, or platform-specific code will reside. This is the right level to test your end-user workflows, e.g. using Behavior-Driven Development (abbreviated BDD), with the help of your Domain experts.
The premise of this Architecture is that it controls coupling. The main rule is that all coupling is toward the center: all code can depend on layers more central, but code cannot depend on layers further out from the core. This is clearly stated in the above diagram: just follow the arrows, and you will find out the coupling order. This architecture is unashamedly biased toward object-oriented programming, and it puts objects before all others.
This Clean Architecture relies heavily on the Dependency Inversion
principle. It emphasizes the use of
interfaces for behavior
contracts, and it forces the externalization of infrastructure to dedicated
implementation classes. The Application Core needs implementation of
core interfaces, and if those implementing classes reside at the edges of the
application, we need some mechanism for injecting that code at runtime so the
application can do something useful. mORMot's Client-Server features
provide all needed process to access, even remotely, e.g. to persistence or any
third party services, in an abstract way.
With Clean Architecture, the database is not the center of your
logic, nor the bottom of your physical design - it is external.
Externalizing the database can be quite a challenge for some people used to
thinking about applications as "database applications", especially for Delphi
programmers with a RAD /
TDataSet background. With Clean
Architecture, there are no database applications. There are applications that
might use a database as a storage service but only though some external
infrastructure code that implements an interface which makes sense to the
application core. The domain could be even decoupled from any ORM pattern, if
needed. Decoupling the application from the database, file system, third party
services and all technical details lowers the cost of maintenance for the life
of the application, and allows proper testing of the code, since all Domain interfaces
can be mocked on purpose.