Down the DDD rabbit hole I go
by Radek Kowalski
“Down the DDD rabbit hole I go”¹ wrote a colleague of mine in a PR². This made me wonder how a technique that was invented to make things easy, makes an experienced software developer feel lost. Let’s explore the Domain Driven Design together and see if we can make you feel a bit at home in the rabbit hole.
Imagine you’re part of a development team, working on an application that solves a particular business problem. The code is nicely structured and you can easily find the piece of code you have to change once you know what is asked of you. You can picture the complete scope of a transaction in your head when a colleague of yours mentions an upsert of that object. You’d be an excellent performing team if only the requirements were clear, there were no scope creeps, last minute changes and that annoying business guy would finally understand that you can’t have the same item twice in a set.
Have you ever experienced anything like that? Does the story above reflect your own thoughts? Should we try to “tackle the complexity in the heart of software³” in 10 simple steps?
The solution to your problem is the completeness and shared understanding of the requirements by the developers as well as the stakeholders.
1. Understand the problem (and don’t jump to solutions).
The primary goal of your software is to solve a business problem. Business problems can be solved in many ways. If you ask the problem owners for the solution, they will suggest ones they had seen in the past, which is not necessarily the best. In a traditional IT organization there might be a chain of people passing the information between the business and the developers. The worst scenarios I’ve seen involved a business person describing the past solution, the analyst noting it down as the requirements and the developers blindly rebuilding the solution described by the business person, including misunderstandings emerging on the line. This disables the teams from understanding the problem their software is supposed to solve.
As a developer, let the product owner, the project manager or whoever with a good network in the organization connect you to the problem owners. Ask the problem owners to describe the problem in their own words. Listen actively and don’t try to correct them. You want to know how they perceive the problem. Make sure you understand the context.
Tip: some business people will try to bridge the gap between them and you by making an effort to talk in technical terms. They will use vocabulary specific to the systems they’re used to. Encourage them to use their natural business language.
2. Build a glossary
You just heard a fascinating story in a language you don’t fully understand. Don’t panic. If you’re in a room with business people from different teams then most likely you’re not the only one confused. Get yourself some coffee and a large sheet of paper. Some good quality stickies might help as well. Now it’s the time to describe the problem together.
Start from building a glossary. Some terms will be new to you, but all the business people will feel comfortable about them. Learn them. Some other terms will trigger discussions between the business people. You definitely want to note that down. Most likely they’re all correct. This might mean that you have just identified a concept that plays a different role for these different teams. Don’t ask them to decide how we’re going to call it (yet). Note down both terms in your glossary, along with the definitions given by both teams. Note down that there is a relation between the terms you yet need to explore.
Don’t focus only on objects. Keep in mind that every action also deserves a good name. What you’re building now is referred to by the DDD as the ubiquitous language.
3. Describe the problem
Using the glossary you have defined together with the problem owners, capture their story in diagrams. Make sure that all the terms you use (including actions) are explained in the glossary.
This is the moment you want to give some structure to the conversation. Guide the problem owners to think in terms of actors, intents, actions and events. There are many methodologies that help you structure this part. Event storming is my personal favorite. Domain storytelling is another good choice.
4. Let the problem owner define your test cases
Who knows the edge cases of the business process you’ll be implementing? Who would come to you with “last minute changes” after they had seen your work in progress for the first time? Who will identify some new edge cases after golive, causing the scope creep? Yes, that’s the problem owner. Let them work for you.
Now that you share the language and the understanding of the problem, let them define the acceptance criteria and the tests. Of course you can’t just ask them to do it, since they wouldn’t know where to start. You have to guide them by asking the right questions.
Very likely the flow you have drafted together is the happy flow⁴. Make them think what should happen if things do not go as planned. Ask what should happen if a particular step in the process fails or (even worse) if it’s taking ages and might never finish. What if the value of a variable is extremely high, negative or a fraction? What if it’s absent? Is the flow the same for all the customers? For all the products? Do you need to be able to adjust the process seasonally or toggle some parts on and off? Capture the answers in the ubiquitous language and note them down in Behavior Driven Development⁵ style. BDD is not strictly part of DDD, but it naturally combines the advantages of the Test Driven Development⁶ with those of the DDD. You want your tests to sound like:
“Given the total shopping cart value of €50 or above, when the buyer finalizes the payment, then a promotional code with 10% discount is issued and sent to the buyer.”
5. Identify the actions and the events
Actions are what you do. Events are things that happen. Things happen for a cause. And you take actions due to things happening. One’s actions are others’ events. What I do as an action, you observe as an event. Behaviour is the way you react to events, the actions you take.
Programmers often focus on objects and their properties when they design software. A DDD practitioner focuses on behaviour. He describes the events happening and the actions that are supposed to be taken in reaction to the events. In object oriented programming behaviour is part of an object. In DDD behaviour isn’t owned by any object in particular (although it’s usually related to one or more of them).
6. Identify the entities and the value objects
Events always happen in a context. There could be a person or a system involved. The event takes place at a certain moment in a particular place.
Most of that context is irrelevant. You don’t want to know it. You want to capture the relevant information about the context of the event in entities. Was an item added to a cart? Which item? To which cart? This is an example of an action (as perceived by the eyes of the actor) or an event (as perceived by the others) along with the entities and the value objects that are part of the context of the event.
Value objects are just pieces of information. Two value objects with identical properties are indistinguishable. They are deprived of identity. They can be as simple as a textual value or contain tens of properties. Still if they’re identical, there is no need to tell one from the other. They never change. They‘re immutable by nature. They can only be replaced. In your domain model they’ll always belong to an entity (directly or indirectly). Universal Modelling Language⁷ would describe the relation between the entity and its value objects as composition.
What differentiates the entities from the value objects is their identity. You can have two identical entities, that preserve their own identity. They can be mutated independently. Each of them can be referenced without any impact on the other. This gives them their very own lifecycle. And lifecycle is expensive. That’s why you want to avoid entities as long as you can do with just value objects.
Don’t confuse logical domain entities with data entities. It’s perfectly normal to represent DDD value objects as data entities in a relational data model. And it’s perfectly normal for value objects in a relational database to have an identifier column, that’s purely technical and should never leak into the domain model.
7. Identify the aggregates and the bounded contexts
Bounded context is the single most important concept in DDD. It’s a logical area where certain language applies and where the entities are allowed to know about each other. It constitutes the boundary of a logical model, that should later be followed by boundaries in the implementation and in the ownership by the development teams. A single team can work on multiple bounded contexts but you don’t want multiple teams to work on the same bounded context.
If you identify an entity that you can’t properly describe without referring to another entity, yet it has its own identity, you have just discovered an aggregation relationship (as in UML) between the two entities. Go over all your entities and discover the dependencies. If you need to mention entity A in order to explain what entity B is, make sure you define what B is without referring to A. Entity B will be your aggregate root. The aggregate root together with the aggregated entities together form an aggregate. Every entity, that doesn’t belong to any aggregate is also an independent aggregate root.
Start from considering every aggregate an independent bounded context. Keep in mind that only the aggregate root may be referenced from outside of the bounded context. If you need to refer to an entity from outside of a bounded context, you need to make it a root of its own aggregate.
Atomic modifications may only happen within an aggregate. If you identify an action that has to happen atomicly across multiple aggregates, you should either consider making them part of the same aggregate or redefining the process in such a way that eventual consistency can be applied.
You want to minimize the size of your bounded contexts to minimize the interdependencies between your entities. If you see a lot of relations between two bounded contexts, perhaps they belong together after all. If you notice islands of entities with just single relations within your bounded context, they make great candidates for separate bounded contexts.
If you notice single entities with lots of events or entities related to them (especially if different people refer to them with a different name), you might be looking at an entity that has to be modelled as part of multiple bounded contexts. It might have a different name and will have a different set of behaviours in each of them. What you call a product in your product information bounded context, might be called “an article” in the pricing context. That’s perfectly fine. Don’t try to convince two departments that they’re talking about the same thing. Most likely they’re not.
8. Define the relationships between the bounded contexts
What impact does the structure of your bounded contexts have on your landscape? Earlier I stated that the boundary of the bounded context is also the boundary of your language. From your holiday experience you might have noticed that two people speaking different languages might find it hard to communicate. Do two bounded contexts encounter the same challenge? How did you manage to ask your French host where the coffee was? Did you use a dictionary? An online translator? What tools does a bounded context have to talk to another bounded context?
In his book Vernon Vaughn describes a number of possible relationships between bounded contexts. They mostly boil down to:
- some bounded contexts being directly aware of the language of the other bounded contexts when referring to the concepts owned by those other bounded contexts
- multiple bounded contexts sharing a language, while preserving the isolation of entities
- existence of an “anticorruption layer”, being the translator, aware of the languages spoken by each bounded context
The type of relationship you define will largely depend on the organization structure of the business and the development organizations. Bounded contexts owned by the same business and development teams will mostly share big a part of the language. Bounded contexts meant to work together but having different owners will mostly be merely aware of each other. You will likely use an anticorruption layer to talk to third party bounded contexts.
Tip: While it’s important to minimize the entity interdependencies by minimizing the bounded contexts, very fragmented bounded contexts will lead to fragmented ubiquitous languages. You want to avoid it by defining a clear relationship between multiple bounded contexts, that will allow you presrve a consistent language across multiple bounded contexts.
9. Map your model to the code
How you map your logical model to the application code largely depends on your own development practices. The most important thing is that your code follows your design.
Starting from the isolation, a model with 10 bounded contexts could be deployed as 10 (or more) microservices as well as in a form of a nicely modularized monolith. What’s important is that your bounded contexts remain isolated (e.g. in form of packages) and that you use well-defined interfaces that expose only the aggregate roots to the other bounded contexts. You don’t want to directly reference entities of one bounded context from another bounded context. If you do it, you end up with a single bounded context, which should be reflected back in the logical model with all the consequences (like using the same language).
Pay attention to the actions you expose on your bounded context. They should be identical with those you have identified together with the problem owner. Use the same names as in the logical model. Accept only those parameters you have described together with the business as relevant parts of the context. Derive as much as you can from other parameters to avoid redundancy or (what’s worse) inconsistency.
A common mistake I observed is building CRUD⁸-based interfaces while trying to follow DDD methodology. CRUD approach, being the foundation of REST inherently conflicts with DDD by being resource-centric and putting an entity in the center of attention. This is a straight path to the anemic model⁹ antipattern. In DDD you want to focus on behaviour, which should exactly reflect the actions described by the problem owners. You don’t want to supply complete entities as the context of your actions. You only want to pass the details that are strictly necessary to perform the action and reference the aggregate root by its public identifier. You can still use all the tools intended for RESTful APIs, but you wouldn’t follow the resource-based URL structure and the HTTP verbs.
10. Don’t try to make everyone happy
“All models are wrong, but some are useful”¹⁰
You will most likely hear that your model is incomplete. You should see it as an advantage. Each model has a goal. Your model should be sufficient to achieve that goal, while remaining as minimal as possible. It’s important to be aware of the greater picture, but it’s crucial to focus on what’s needed now. You’ll need to iterate. And add only the things you need. What about the rest? YAGNI¹¹.
- I’m pretty sure that this particular fan of Alice in wonderland meant something like “there’s weird stuff about to happen”
- Pull Request — a mechanism to discuss the code changes between developers
- Domain-Driven Design: Tackling Complexity in the Heart of Software — the name of the famous DDD book by Eric Evans. Highly recommended (but beware, you won’t finish it in a couple of evenings!)
- Happy Flow — a default scenario that focuses on what we want to happen and ingnores any exceptional situations
- Behaviour Driven Development — a practice of business people collaborating with the developers on building the tests upfront
- Test Driven Development — a practice of having the tests written before writing the actual application code.
- Universal Modelling Language — a modelling language aiming at having a standard way to represent software systems and processes.
- Create Read Update Delete — the four basic operations performed on resources. A very useful concept for abstracting the persistence layer from the application logic.
- Anemic model expresses itself with entities that just contain data and lack logic. Since you need the logic in the end, it ends up either being built outside of the bounded context of the related entity or being written in procedural style and not exposed as an action on the interface of the bounded context.
- A famous quote from the British statistician George E. P. Box
- You Ain’t Gonna Need It — a practice coming from Extreme Programming methodology. It helps you keep the software simple by discouraging building overly generic features that try to anticipate the future requirements.