Practicing Domain Driven Design. Part 2: Dividing the problem.
In my previous article, we covered the process of understanding the problem using an event storming session. I strongly recommend that you to read it first before continuing.
In this article, we will try to divide our problem into sub-problems.
In DDD terminology, those are called Bounded Contexts.
Why is this division important?
One of the most powerful techniques for solving complex problems is to break them down into smaller, more easily solved pieces. Smaller problems are less overwhelming, and they allow us to focus on the details that are lost when we are studying the entire problem. Additionally, from a software perspective, we get benefits like:
- The freedom to independently develop and deploy
- The possibility to use different programming languages, databases, hardware and software environments for a specific sub-problem
- A small piece of functionality is easier to understand, so new developers become productive quickly
What is a bounded context?
A bounded context is a logical boundary around a specific business functionality (a very common mistake is to define boundaries based on data but not around the functionality that the data serves). We keep all the knowledge inside the boundary and ignore the noise from outside. The terminology within a bounded context has a single, clear definition that accurately describes the problem.
Bounded contexts allow us to split a big problem into smaller problems so we can focus on specific aspects of the application. They can communicate with other contexts and share the data.
To see the big picture of the system we build a context map. A context map shows how each bounded context relates to the others.
How to identify a context?
As we already know, a bounded context is a logical boundary, so a lot of decisions will be logical in the first place, and there are no concrete steps that work for every problem. But there are some hints and techniques. When identifying boundaries between contexts, always keep in mind the rule of cohesion: things that change together and are used together should be kept together. We want to build a highly cohesive system.
Cohesion — the degree to which the elements inside a module belong together.
On the other side we have coupling:
Coupling — the degree to which modules know about other modules.
By definition, you can say that high cohesion is a good thing and high coupling is something you want to avoid. One of the main goals of bounded contexts is to achieve low coupling and high cohesion. A good metric to measure this is the number of services you need to touch when adding a new feature.
Building bounded contexts
Here are some hints we will use to build bounded contexts for our problem:
- The same definition used in different places is a good sign of a boundary between them (Different contexts may have slightly different meaning for the same term)
- You may add a boundary around transitions of the specific data in a timeline. (In our case Question is a good example of data that has transitioned in its lifecycle: first, we add it to the system, then it’s given to the user)
- Every third-party system should live in a separate context (This could be the client app or some third-party data providers).
In the previous article we have visualized our domain on the wall using sticky notes. Now, our goal is to define bounded contexts.
Following the first hint, you probably have noticed that we have Question defined two times in different places (‘Question added’ and ‘Question given’). Is it the same Question? It may have a similar or even identical structure right now. But imagine we need to implement support for multiple languages. Then, when adding the question, we need to extend it with data about supported languages. At the same time, Question that is given to a user remains the same. In the moment of answering a specific question, a user does not care about other supported languages. In one case Question is about having a wide range of content in the system, and in another, it’s about validating user knowledge. We have different processes here, and the meaning of a Question is not the same. This is our first boundary:
Next to the Question we have Category; do they belong to the same context? Remember the rule of cohesion: things are used together should be kept together. A Category is a group of questions and it does not make sense without Questions. So they should definitely be in the same context, I will call it Question store. The problem we want to solve within this context is managing question-related content in the system.
We know that there is another boundary around the ‘Question Given’ event.
The only reason we ask questions is to get answers. Question and Answer are part of the same process, so events like ‘Question given’ and ‘Answer given’ should be in the same context: Question answering, where we cover the process of giving questions (from the system) and answers (from the user).
What about the progress? Technically you can answer questions without tracking the progress. We do not really focus on that in this article, but potentially you can use it to build recommendations for a user or to find the most answered questions, etc. That’s why I will create another context: Progress.
Context Mapping
Now we need to define context mapping: how contexts communicate with each other. To do this, we need to understand if the cause alone is sufficient for the domain event to occur.
Let’s start with ‘Question store’. The cause of each event is a command, and it looks like the command (and its arguments) is enough for the event to appear. For example, the ‘Create Question’ command will have a Question as the argument to create that question. ‘Find Category’ with a keyword as an argument can make ‘Category found’ happen.
What about ‘Question answering’? From the previous article, we remember the ‘Get Question’ command comes with Category as an argument and it’s not enough to get the next question. First, we need all available questions in the category. Also, to find the best question for the user, we need his progress. This context has to communicate with other contexts.
- We can ask the ‘Question Store’ context for available questions in the category
- We can ask the ‘Progress’ context for the current progress
- When a user answered, we need to notify the ‘Progress’ context if the answer is correct
So far we did not talk about the client app. Should it be a web page, a mobile or voice app? Well, we can have multiple clients, and we can make that decision later. In the end, it’s just a way of presenting the data. All commands we have on the board should come from a user (except ‘Create Question’ and ‘Create Category’ as we know from the previous article), so we need something that the user can use to interact with the system. We can add the client as separate context to complete our board.
If we put everything together on the board:
Finally our board is ready. What did we achieve?
- We have split out problem into smaller problems so we can tackle them more easily
- We identified dependencies between services
- We defined how our services should communicate with each other
Developer’s perspective
Having software functionally decomposed really helps during its development and maintenance. From a developer’s perspective, a bounded context is an independent module or service with some API (but keep in mind, the single context could result in multiple independent micro-services). So far:
- We still did not write a single line of code;
- We know how our models should look like;
- We defined interfaces between our services/modules;
In the next article, I will cover the implementation process, so stay tuned.
P.S. I hope it’s clear I did not cover every detail in order to keep the article simpler and beginner friendly.
Learn More: