Domain-Driven Design (DDD) has become a thing. What's all the fuss about?
In this article I summarise, as succinctly as I can, the main concepts and principles behind Domain-Driven Design.
When we write a software application, the normal process is to take a well-known architecture such as Model-View-Controller and to force the application to fit it. Domain-Driven Design inverts that. The design of a system is inspired primarily by the business domain in which the software operates.
The core idea behind DDD – as described by Eric Evans who introduced the concept in his book "Domain-Driven Design: Tackling Complexity in the Heart of Software" – is that good software closely mirrors the human processes that it automates or the real-world problems that it solves. This is known as the domain or problem space of the software.
Architects design buildings to serve specific functions. The blueprints for a public library and a private home will therefore look very different. Domain-Driven Design takes this principle and applies it to software. The blueprint for a banking application should look like the process flows of a bank.
Domain-Driven Design is all about good domain modeling. Software that is not carefully crafted to serve the specific domain for which it is intended will introduce inefficiences into the existing human system.
DDD really lends itself to projects where the domain is non-trivial. Think banking applications and air traffic control systems. DDD is overkill for small and simple software projects.
How do we take real world processes and write software applications that fit them harmoniously?
The answer is to gather requirements from the people who know the most about those real world processes: the domain experts. The role of the software engineer is to make software that the domain experts would make if they were coders. The responsibility of the software engineer is to deliver business value, rather than technological solutions. So, DDD requires that software engineers have access to domain experts, who may be the ultimate end users of the application. The experts and the engineers collaborate through continuous conversation and feedback to create a model of the problem domain that makes sense to all of the project's stakeholders. The goal is for the resulting software to closely map how the business itself thinks and operates.
DDD is normally an iterative and incremental process. DDD is less effective in waterfall processes because it is so difficult to create from the start a complete model that covers all of the required business needs.
The emphasis on continuous conversation and close collaboration between stakeholders brings other advantages. Often the process of automating business processes through software engineering can yield new insights into the business itself. The constant delivery process brings more insights over time. And it serves the purpose of centralising knowledge within an organisation, so that understanding of certain aspects of the business are not restricted to particular tribes within it. Knowledge is dissipated throughout the organisation.
Before a domain-driven project gets underway, all of the project's stakeholders agree on a common language. Rather than the domain experts and the software engineers each using their own industry jargon, a shared vocabulary is developed that all parties understand.
This common language is sourced from the domain's real world concepts and terminology, and it is applied ubiquitously throughout the project, from the UML's through the tests to the source code itself. The ubiquitous language need be nothing more complicated than a glossary of nouns and verbs that can be used together to describe all aspects of the domain.
The problem domain is modeled using the ubiquitous language.
A model is a system of abstractions that describe all aspects of a domain. The model is the essence of the software. Models help us to deal with essential complexity and are one of the most powerful patterns found in scalable software design.
Models may be expressed in documentation, code, tests, or visually (e.g. UML), or a combination of all of these things. Whatever the medium used to express models, the most important criteria is that a consistent language is used throughout.
Ultimately, the models will be implemented in code. So the modeling process needs to consider not only the domain but also its implementation in software. It is important that models can be translated into working code. It is possible that a model that is truthful to the domain could have serious problems in its implementation, such as an unacceptable performance hit. For this reason, domain modeling and code design should be closely related.
Throughout the implementation phase, the software engineers make frequent references to the model. The software system should map the model in a very literal way, going as far as to use the same ubiquitous language for class and method names. Developers should feel ownership of the model and responsibility for its integrity. If the model changes, the code should change too, and vice versa.
For obvious reasons, object-oriented programming is well suited to model-driven design, more so than procedural and other paradigms that do not provide sufficient constructs to reflect complex models.
If a project is large and complex enough to have multiple subdomains, it will have multiple models. Each model should be fully contained within a well-defined "bounded context" that reflects the subdomain. A "context map" may be used to visualise the dependencies between each bounded context. It is difficult to keep models pure when they span the entire scope of an enterprise-level project, but it is much easier when models are limited to narrowly-defined areas. The boundaries between subdomains may be reflected in team organisation and other physical manifestations such as codebases and database schemas.
In my opinion, the principles of DDD are more important than any of the particular architectural styles and design patterns that are commonly described as being part of the DDD toolkit. Nevertheless, below is a summary of some of the common building blocks found in domain-driven software applications.
DDD is said to require a layered architecture to keep the domain isolated from its actual implementation in application-level and framework-level code. When business logic gets scattered throughout a codebase – embedded in UI widgets and database scripts – the system becomes hard to maintain. Automated testing is awkward and changes tend to need propagating throughout the whole software stack. For example, changing a business rule may require meticulous stack tracing of behaviour through database code, UI elements, and other components.
The solution espoused by Domain-Driven Design is to partition complex programs into clearly separated layers. In particular, domain logic is kept entirely separate from its concrete implementation and especially infrastructure-level code such as database connections and filesystem processes. Domain-driven software is commonly described as having three main conceptual layers:
- The foundation domain layer is generally small and holds the state of business objects. The domain objects are free from responsibility for displaying themselves, storing themselves, or managing application tasks and processes. These responsibilities are delegated to other layers. For example, persistence of the state of domain objects tends to be delegated to generic components, such as database abstractions, in the infrastructure layer.
- The application layer is another thin layer that coordinates application tasks and processes, but does not hold any state itself. The application layer may be seen as a mediator between the domain and presentation layers.
- The outermost presentation or user interface layer deals with input and output. It is responsible for interpreting user commands and presenting information to the client.
In addition, an infrastructure or framework layer, which is kept isolated from the application code, acts as a general supporting layer for everything else.
The idea is that each layer should depend only on the layer immediately outside of it. So, the domain layer should not get involved in application tasks and processes, and the UI should not be tightly connected to the domain logic.
Standard design patterns are used at the boundaries between layers to promote loose coupling between them. Interfaces and adapters are commonly used at the boundaries for this purpose. Plugins, facilitating event-driven processes, are another commonly used design pattern. The more that the dependencies between the layers of the application can be reduced, the less likely that business logic will get tangled up throughout the stack.
Entities and value objects
In DDD, models are commonly expressed as entities and value objects.
Entities are a class of object whose identity remains consistent throughout the lifetime of the system, even if they are created and destroyed and even if their attributes change. A good example would be an entity type that represents bank accounts. Bank account numbers provide a unique persistent identifier for each entity. You could not model people as entities – two people can share the same attributes such as name and age – unless you have a unique identifier for each person, such as a social security or passport number.
Modeling using the entity pattern helps to enforce data integrity.
Not all data objects need to be entities, because not every data object needs a unique identity or needs to persist beyond its destruction in memory. If all we are interested in are the values or attributes of something, then DDD principles state that value objects are preferred. Value objects work best when they are immutable, created exclusively through their constructor and not further modified throughout the remainder of their lifetime.
A model may be made up of a large number of entities and value objects with many complex relationships between them. The challenge of modeling is twofold: to make the model complete, but also to keep it easy to understand. To meet the second challenge it may be necessary to group related data objects into aggregates. An aggregate is a group of related entities, value objects, and other relevant objects. The aggregate group will have one root entity that provides a single point of entry to the whole group. An example of an aggregate would be a
Customer entity that internally references lots of other entities and value objects that represent things like
EmailAddress, and so on. From an external point of view, the Customer entity is a single unit of code, simplifying the overall system and helping to maintain data integrity.
Factories and repositories
A factory is any kind of class or method that is dedicated to creating other objects. This is a useful design pattern when a data entity or value type becomes very complex to construct. If the construction of an object requires knowledge of the object's internal structure, it is good practice to create a factory to simplify and abstract away the assembly operation. Factories encapsulate the knowledge needed for complex object creation.
While factories are involved in the creation of objects, repositories are involved in their storage. A repository is an abstraction of a persistent container or storage layer. The great advantage of this design pattern is that it helps to hide infrastructure details, such as database credentials. A repository encapsulates all of the logic needed to obtain references to a particular type of object. Different repositories may persist objects using different "strategies" and they may even cache some stuff locally. The domain-level code will never know the implementation details. Repositories create the illusion of simple in-memory collections.
Entities tend to be accessed via repositories while complex or aggregate value objects tend to be manufactured by factories.
Services do not have attributes or internal state, but they do exhibit behaviour. Their role is to facilitate processes and tasks. Services may operate in different layers, for example there may be infrastructure-level services that perform tasks like sending email and persisting data, and domain-level tasks that do things with your core business objects.
Consider for example the task of transferring money from one bank account to another. Putting such behaviour in the entities for the sending or receiving account feels misplaced. Such tight coupling is a code smell, a sign of poor design that makes it hard to understand and change the system. A better design is to keep the behaviour separate, encapsulating it in a service object that acts upon both entities – for the sending and receiving account – simultaneously. This design fosters low coupling, high cohesion and code reuse.
It is best practice to extract services when:
- The operation does not fit naturally within any particular entity or value type.
- The operation acts upon multiple objects.
- The operation is stateless.
Service operations may be triggered in any number of ways. Some of the DDD literature emphasises the use of event-driven architecture to decouple things further still.
Finally, a module is a package of related entities, value types, factories, repositories and services. This design pattern becomes essential when a model grows so large as to be difficult to talk about as a whole. Modules group related concepts and expose a single interface through which they interact with other modules, substantially reducing complexity of the overall system.
Domain-Driven Design is a powerful set of concepts and tools for designing the internal mechanics of software systems. The ultimate goal of Domain-Driven Design is to allow an application to organically grow into a shape that snugly fits the business needs, by mirroring the organisational structure of the real-world business itself.
In any non-trivial application, this is a far better approach to software design than trying to shoehorn the application into a rigid pre-determined architecture. Unlike architectural patterns such as MVC, Domain-Driven Design is a more of a conceptual framework than a strict classification system for source code.
I think that the most powerful feature of Domain-Driven Design is that it allows the development of software systems to be equally driven by developers and the customer or end users. And it encourages developers to put most of our energy into the design of the software's essential complexity, the stuff that is essential to the business itself.