Domain Driven Design
TL;DR: Domain Driven Design gives a set of guidelines and techniques that we can use to try to break larger domains into smaller domains.
The problem that we have with large domains is that they can be very hard to model. There are a lot of rules, a lot of different things that we have to keep in mind when we're trying to model a large domain. Trying to do that all in one single coherent model can be very difficult or impossible in many cases. Because of that, we try to break things down into small pieces. This gives us a way to determine boundaries essentially between those smaller pieces within the larger domain.
Microservices, reactive microservices specifically, have a similar goal: they need to be separated along clear boundaries. In the case of reactive microservices those boundaries need to be asynchronous: each microservice has to have a clearly defined API and a specific set of responsibilities. If we don't know what the responsibilities of the microservice are, then it's going to be very hard to build it and design it properly. The trick comes when we try to determine what those boundaries are. There's no clearly defined technique that will just give us the right answer all of the time; however, this is where domain driven design can help us. It does give us a set of guidelines and techniques that we can use to try to break larger domains into smaller domains.
Domain driven design can be used in the absence of a reactive microservice or a reactive architecture. And you can build reactive architectures without domain driven design, but because the two are very compatible, you'll often find them use together
What is a Domain
A domain, if you look up the dictionary definition, is going to be a sphere of knowledge. If you're talking about a domain, you're talking about a specific area of knowledge. In the context of software that changes a little bit, but at its core, it's still the same basic meaning. What it means in the context of software is it's referring to a business or idea that we are trying to model. Typically when we build software there is some sort of concept or business that we're trying to model, and that is our domain. Experts in the domain are therefore people who understand that business or that idea, not necessarily the software.
The key goal of DDD
is to build a model that the domain experts can understand. The model is not the software. The model represents our understanding of the domain. The software is an implementation of the model.
Communication between developers and domain experts require a common language. This language is called the Ubiquitous Language
.
Terminology in the Ubiquitous Language comes from the domain experts.
- Words originate in the domain and are used in the software.
- Avoid taking software terms and introduce them into the domain.
Domain experts and software developers should be able to have a conversation without resorting to software terms.
For example, avoid “entity, database, event bus, etc.” and try using “menu, cooks, dishes, etc” instead.
Decomposing the Domain
What we do in domain driven design is we take our large domains and we separate them into sub domains. Our sub domains are created by grouping related ideas and actions and rules into a separate sub domain. If we look at our restaurant again we had all those different ideas but some of them are related. For example, we can look at a restaurant reservation: table, customer, time, location, etc. Those are all specifically related to creating a reservation.
We might look at that and say "well perhaps there's a reservations sub domain that deals just with creating reservations within our restaurant". We kind of extract that out into a separate set of terms and a separate ubiquitous language. In fact we can extract it out into a separate model. What we may realize though is that some parts of that may exist in multiple subdomains. Customer, for example, is probably going to exist in other subdomains as well. It's not strictly limited to reservations. We have to recognize the fact that some of these concepts can actually cross multiple subdomains.
It's also important to realize that those shared concepts, such as customer, may not be identical initially. They may actually look different. They also may look initially the same, but then evolve differently over time. Because of that it's important to avoid the temptation to abstract. We may have a temptation to take the concept of customer and say that we just have one concept of customer. And it always looks the same, no matter what sub domain we're working in. But the reality is, customer in, for example, the reservation sub domain may have details that are unimportant in other parts of the business. Or it may not need details that are important in other parts of the business. Because of that each sub domain essentially ends up having its own ubiquitous language and model. The language and model for a sub domain is what we call a bounded context
.
From one bounded context to the next, the meaning of a word may change dramatically. In a restaurant, for example, we're talking to a server, the term order has a very specific meaning. When you talk to the server it means one thing. However if you talk to somebody else in the restaurant, for example, the person who manages inventory, then order means something different. When you're talking to a server, an order is something that is very much client facing. The customer comes in, they place an order and the restaurant provides them with food. In that case, the customer, they represent the customer. Whereas the restaurant represents the supplier for that order.
How do we decide what is a bounded context and what isn't? Where do we decide to draw those boundaries? There's no universal answer, just guidelines:
- Consider human culture and interaction. Different areas of domain that are handled by different groups of people.
- Look for changes in the Ubiquitous Language.
- Look for vary or unnecessary information.
Strongly separated bounded contexts will result in smooth workflows.
- An awkward workflow may signal a misunderstanding of the domain.
- If a bounded context has too many dependencies it may be overcomplicated.
Event First Domain Driven Design
- Traditionally, DDD focused on the objects within the domain. Eg. Cook, Reservation, Customer, etc.
- Event First DDD places the focus on the activities or events that occur in the domain. Eg. Customer makes a reservation. Server places an order, Food is served to the customer, etc.
- Using Event First DDD we start defining the activities, then group those activities to find logical system boundaries.
To identify the defining activities, there is a tool called “Event storming”.
Maintaining Purity
Once we've separated our bounded context into these nice clean boundaries we have a bit of a job ahead of us which is maintaining those clear boundaries, maintaining the purity of those bounded contexts. We need a technique or a set of techniques that allow us to do that. One way that we do that is with something that we call an anti-corruption layer
. When we have our bounded context, it's important to recognize that each bounded context may have domain concepts that are unique.
What we don't want is we don't want to just come up with sort of an abstraction or some way to make it the same across all bounded contexts because what that does is it causes one bounded context to bleed into another. For example, if we just said "we have a customer representation that includes all the details: it includes address, phone number, when they became a customer, etc". We could just use that representation of a customer everywhere and then we don't have to worry about that. But that creates coupling that we want to avoid.
For example if we do that and in some moment in time we change the structure of an address (maybe it was represented one way and then we realize there is a better way to represent it, we include additional information or we remove some information). If we've used that everywhere then now we need to update the reservations context even though the reservations context doesn't care about the address. So now we've created an unnecessary coupling and we want to avoid that.
The anti-corruption layer allows us to avoid that. What it does is it looks at whatever that customer context representation is and it translates it into a representation that is unique to the reservation service. It strips out the unnecessary information like address. As a result, the reservations context only deals with the pure representation that it cares about. This prevents bounded context from leaking into each other but it can also allow our bounded context to stand alone.
Context Maps
Context Maps are a way of visualizing Bounded Contexts and the relationship between them.
- Bounded Contexts are drawn as simple shapes.
- Lines connect Bounded Contexts to indicate relationships.
- Lines may be labelled to indicate the nature of the relationship.
Decomposing the Domain
Domain Activities
Within a domain, there's actually many different types of activities that we could be dealing with. We're gonna go into some specific categories of activities. One of the types of activities that we deal with in domain driven design is called a command
. A command is a type of activity that occurs in the domain. It represents a request to perform an action.
It's important to understand that because it's a request, it's not something that has happened yet. It is something that we are asking to happen in the future. And, because it's a request, it could potentially be rejected; we could choose not to proceed with whatever that request is.
Commands are typically delivered to a specific destination. They usually have a specific recipient in mind whether that's a specific microservice or something else. And when it is received, it will cause a change to the state of the domain. After the command has been completed, the domain won't be in the same state that it was prior to issuing that command. Some examples of commands would be: "add an item to an order," "pay a bill," "prepare a meal.", etc.
In addition to commands, we also have events
. You'll see why we were differentiating earlier between the term events and activities. Events in our case is a type of activity. It's a very specific type of activity. An event is an activity that occurs in the domain but now it represents an action that happened the past. Because it happened in the past, it can't be rejected. You can't say that an event never happened because it did. It is part of history at this point. You can choose to ignore it. You can choose to do nothing with it. But you can't say that it never happened. Events are often broadcast to many destinations. So where a command is usually message going to a specific recipient, an event is often just sent to anybody who cares. You will broadcast it to many different microservices or many different destinations. What an event does, where a command will cause a change in the domain, an event actually records a change in the domain.
And finally we have queries
. Queries are the final type of activity that we're going to talk about. They represent a request for information about the domain. As you'd expect with a query, you always expect a response. With a command or an event, that's not necessarily the case. Commands you often expect a response, basically something like "yeah I got your command." You may not actually get any details in that response, it's just "yes I got that and I will do that." Events, you may not get any response. Sometimes you will get an acknowledgment like "yes I got that." With queries though, you're always expecting some sort of detail back. You don't just want a "yes I got your query." You want an answer to your question. So a response is always expected.
The other thing is that queries should not alter the state of the domains. When we do a query, if we do repeat it multiple times, we should always get the same response assuming nothing else has changed. We should never get a situation where we issue a query and that query changes the state in some way. If we do change the state in some way it's not a query anymore it's now a command.
Commands, Events, and Queries are the messages in a Reactive System. They form the API for a Bounded Context or Microservice.
Domain Objects
When we start diving in more deeply into a bounded context. One of the objects that we're going to encounter is what we call a value object
. A value object is defined by its attributes. A value object must be immutable. These are typically state containers and can also have business logic.
Messages in Reactive Systems are implemented as Value Objects.
To contrast that, we also have something called entities
. An entity is defined by a unique identity like an ID or a key. All the attributes that are not unique identities can change.
- Entities are mutable.
- Entities are the single source of truth for a particular id.
- Entities can contain business logic.
There's also a specific type of entity which we call an aggregate
. An aggregate is a collection of domain objects bound to a root entity.
- The root Entity is called the Aggregate Root.
- Objects in an Aggregate can be treated as a single unit.
- Access to objects in the Aggregate must go through the Aggregate Root.
- Transactions should not span multiple Aggregate Root.
- Aggregates are good candidates for distribution in a Reactive System.
How do we determine aggregate roots? Aggregate roots are very important in a domain driven design system but figuring out what they are is not always straightforward. There are a few problems. One is, the aggregate root can be different from one context to the next. You can encounter situations where a context may require multiple aggregate roots. It's not common, it's far more common to see a single aggregate root per bounded context but it's not always the case.
Some questions to consider:
- Is the entity involved in most operations in that bounded context?
- If you delete the entity, does it require you to delete other entities?
- Will a single transaction span multiple entities?
Domain Abstractions
In addition to the activities and objects that occur inside of our domain, there are certain abstractions that we leverage as well when using domain driven design. These abstractions can be useful for a number of different reasons. One of the abstractions that we use is what we call a service
.
We talked about the fact that we have entities and value objects. And that those entities and value objects can contain business logic as well as state. But sometimes you get cases where there's a particular piece of business logic that doesn't really fit into an entity or a value object. For whatever reason you can't come up with an entity that makes sense for that particular piece of business logic. In that case, this can be encapsulated by a service. Services have some special criteria: The first is, they should be stateless. The other thing is that services are often used to abstract away something like an anti corruption layer. Too many services leads to an anaemic domain. Look for a missing domain object before resorting to a service.
Another type of abstraction that we use is a factory
. When we want to create a new domain object -- often entities or aggregate roots -- the logic to construct those domain objects may not be trivial. There may be a lot of work involved with creating that domain object. It may have to access external resources. For example, when we create a new "Reservation" maybe we need to assign an unique identifier to it, maybe that requires us going to the database and looking at a table. There may be a database access we might have to look at, files or REST API's. There are all sorts of complexities that may come along with building that new object. A factory allows us to abstract away that logic. It's usually implemented as a domain interface with one or more concrete implementations.
Factories, in the term "CRUD" -- Create, Read, Update, Delete --, are the "C".
Repositories
are similar to factories but, instead of abstracting away creation, they abstract away the retrieving of existing objects. Factories are used to get new objects. Repositories are used to get or modify existing objects.
Repositories, in the term "CRUD" -- Create, Read, Update and Delete --, are the "RUD".
They often operate as abstraction layers over the top of databases, but they can also work with files, REST API, etc.
A repository does not automatically imply a database.
Factories and Repositories are related. For this reason, they are often combined.
A repository may end up with all of the Create, Read, Update, & Delete operations.
Hexagonal Architecture
A particular technique that is often combined with domain-driven design is called hexagonal architecture
. Hexagonal architecture is not directly related to domain driven design. You can use domain driven design without using hexagonal architecture; however, it is very compatible with domain driven design.
What is hexagonal architecture? It's also known as ports and adapters
. The idea of "ports and adapters" and "hexagonal architecture" was proposed by a man named Alistair Cockburn. Basically, it's an alternative to the layered or n-tier architecture that you may or may not be familiar with. The n-tiered architecture, if you're not familiar with it, is the idea that you separate your application into different layers. Usually with a database layer at the bottom and there's some kind of user interface layer at the top with kind of a domain layer sandwich somewhere in the middle.
The idea is that you have the user interface on one end, the database on the other end, and a bunch of other layers in the middle with the domain being one of the important ones. Hexagonal architecture takes a sort of a different approach here. The idea here is that instead of the domain being in the middle, is at the core of something that more closely resembles in this case a hexagon. The domain becomes the core and it becomes the most important thing in your application. Ports are exposed by the domain. Those ports act as an API. They are the preferred way of communicating with the domain. Then you have a series of infrastructure adapters. And those adapters communicate with those ports. They communicate through that API.
There are different sides to the hexagon. On one side we have something like an "user" side API and on the other side we have the "data" side API.
Hexagonal Architecture can be viewed as an onion. Domain is the center of the onion. API provides an interface to the Domain. Those are your ports. Infrastructure adapts incoming and outgoing traffic into the ports.
Outer layers depend on inner layers. Inner layers have no knowledge of outer layers.
Hexagonal Architecture:
- Ensures proper separation of Infrastructure from Domain. Prevents concerns around databases, user interfaces, etc. from bleeding into the domain.
- Can be enforced with package, or even project structure in the app.
- Allows your domain to be portable.