Large complex systems tend to be difficult to maintain. They never start out like that. They start small, sweet and innocent. However, the system evolves. Features are build on top of features. The world around it changes. Functional requirements change as business changes, but also the organisation, the teams and the people. When a system evolves into a monolithic style system, it becomes increasingly difficult and expensive to adapt it to the altering world. By designing the system as several smaller delimited systems that work together, you can create a system that better reflects the business and is more flexible to change.
As developers we all have experienced these growth pains when our code needs to scale. During development, we constantly ask ourselves questions like:
- Where do I need to put class/type x? What module, namespace etc?
- Do we need a module/package for this part of the code? What needs to be shared between projects?
- When does the code become too complex and needs refactoring?
- When do we need to alter the design of our system?
- What do we name this class/type/module etc.
Without some common design concepts, a system can easily grow into a big ball of mud. I usually advise development teams to think about bounded contexts as soon as possible. This design concept was introduced by Domain-Driven Design.
As developers we tend to abstract things. While creating common types, classes, inheritance and modules, we share code and functionality among projects. We use patterns like dependency injection to make our code technically loosely coupled and testable. What I have seen happen so many times now is that types are grouped by category, rather than functionally, consequently causing that everything becomes functionally tightly coupled. i.e. a monolithic system. In such a system functionality is not easily discoverable. Developers are afraid to change code when it’s difficult to foresee the impact of their change on system behaviour (hopefully everybody thought about writing unit tests!). And it doesn’t only happen with on-premise, N-tier kind of applications. I’ve seen distributed cloud applications coping with the same issues. For example, multiple services in an application might be tightly coupled through code libraries which contain common logic. The trade-off that people should always consider then is that although it gives consistency of code across all services, it also diminishes agility. If one of those library gets updated, all dependent services need to be updated and redeployed. One way to reduce the risk to end up in a situation like that, is to introduce bounded contexts. Developers and architects should look at the domain model they are trying to implement, divide it into separate subdomains and set clear boundaries with interfaces between them. Within those boundaries a team can autonomously develop their part of the system. With respect to the interfaces. Let me elaborate on this.
Modelling bounded contexts
Let’s look at a model of a retail store, containing a few entities. Note that a domain model is a system of abstraction. Not a data structure, object diagram or anything like that. An entity is a noun, a thing that represents some concept from the real world:
The store sells products, but also purchases them to supplement their stock. So which type of product is defined by the Product entity? What’s the difference between a Sale and a Purchase? The term ‘Purchase’ might be introduced in the model because ‘Sale’ was already taken, thus creating a discrepancy between the model and the real thing. As you can see, things might quickly become unclear. What would happen if we divide this model into subdomains? Let’s see:
This model still contains everything from the previous model, some entities are renamed in order to closely resemble the real domain. Sales and Marketing both have an entity called Product, but for each subdomain they mean different things. Also, a marketing specialist in the store has a different understanding of what a sale is, then a person who works at the sales department. The same goes for cash. As such, you can state that the meaning of a word can be different depending on its context. Purchasing and Marketing may give the same meaning to the concept of Product. However, it can be that in Purchasing it might contain a SupplierId or something, but that is irrelevant in the context of marketing. Therefore you can also state that an entity can be modeled differently in separate contexts. The object Discount Action is placed in the Marketing subdomain and is renamed to Sale. There is no name collision with the Sale object of the previous model anymore, as that exists in the Sales subdomain. You can use the same name now for different concepts.
Each subdomain contains its own model specific to that subdomain. They have the ability to evolve separately from each other and, in the best scenario, are developed by separate development teams. These teams should include (sub)domain experts, preferably. The model also has its own ubiquitous language. I wrote about ubiquitous language in my previous blog post.
So… a bounded context is the same as a model of a subdomain?
Well, almost. Here’s what Eric Evans wrote in his book:
“A BOUNDED CONTEXT delimits the applicability of a particular model so that team members have a clear and shared understanding of what has to be consistent and how it relates to other CONTEXTS”
From: Eric Evans. “Domain-Driven Design: Tackling Complexity in the Heart of Software”. Chapter 14, Maintaining Model Integrity.
The “applicability of a particular model” here means everything the model applies to. The bounded context is a logical concept that places clear boundaries around everything concerning the subdomain the model applies to. Everything! The model, but also the team, the ubiquitous language, the physical resources (e.g., cloud services), build scripts, deployment pipelines. It is the clear logical boundaries that delimits one context from the other.
Some concepts — whether an entity, service, build script or something else — may exist in more than one subdomain. You might be tempted to share these between the bounded contexts. However, I would advise to be cautious doing that. It increases cohesion between contexts, preventing them to evolve autonomously. Also, same concepts may be modelled or implemented differently, so be careful abstracting these out. It’s often a matter of consistency vs agility.
Hopefully this explains to you what a Bounded Context is. But how do you define them?
Unfortunately there is no recipe for this. However I can give you some tips
- Look for natural boundaries.
Often if you look in the domain, you may see that different parts are handed by different teams or departments. Observe how those groups of people interact with each other. Look at the cohesion between people.
- Follow the language.
When you notice that the terminology in the ubiquitous language changes from one domain expert to another, this might indicate you’re crossing from one bounded context to another.
- Follow the information.
If you find out that something in a certain place is explained in a particular manner, but differently somewhere else, then it could be you crossed a boundary between contexts. In the previous example this was the case with the Product entity.
- Don’t overly complicate things
Check dependencies between bounded contexts. If a bounded context has too many dependencies it could mean you’re making things too complicated. It might be a good idea to merge one or more of your defined bounded context in a single one.
- Watch how development teams work.
If things become too complex, you’ll see more overhead between teams. When teams become too dependent on each other about the model they use, it might mean you need to revisit your bounded contexts. It could also mean people are misunderstanding the domain.
- External systems are always modelled as bounded contexts.
These systems have their own model(s) of which you have no direct control. Therefore you should isolate them in their own bounded context.
Maintaining integrity between bounded contexts
Once you have more than one bounded context in your application, it’s sensible to draw a context map. By drawing a context map you see how the bounded contexts influence each other. It gives you a holistic view of the system with its contexts and their relations. Let’s create one for the retail store, with some external systems added to the diagram.
The diagram shows which contexts are related and how. The D/U attributes depicts a dependency between a downstream and an upstream context, where the upstream context is the influencer of the downstream context. This can be because of the code/model, but the reasons can also be more of a behavioural nature, like relying on scheduled events for example. Note that it isn’t necessarily about the direction of information, i.e. information can flow from downstream to upstream contexts while the upstream context still defines the model of that data. When there is an equal relationship, you have a partnership relation. Both contexts work equally with each other for common goals. Of course this requires more cooperation between development teams. If cooperation is limited – because of location for example – it might be feasible to think deeper about this relation. When the context has relations with external systems, it might very well be the case that you don’t have any say-so in the relation. External API’s can be offered as-is and change when the owner wants it to. In such case the downstream context has a conformist relationship with the upstream context.
Contexts always bear the risk to be susceptible to model-leak from another related context. Allowing that places a burden on the autonomy of the bounded context. DDD has a pattern called Anti-Corruption Layer (ACL) that can be used to perform mapping and translation [of the model] from one context to another. For mental illustration: you can think of a repository that gets product data to update the information in the Purchasing context. This repository maps the information modelled by the Wholesale Supplier to the model used within the context of Purchasing, before it further enters the Purchasing context. In this case, the repository is part of the Anti-Corruption Layer.
Defining bounded contexts and mapping them is a valuable concept to use when designing your system. It provides a tool you can use to maintain a holistic view of your system. It assists in making technical, but also managerial decisions. While something is technical feasible, it might not be manageable. Pushing technical feasibility in that case can jeopardise the success of the project, unless you also make appropriate managerial changes. It helps you create teams and designs that are optimised to work and evolve as autonomous as possible. While there is no fixed recipe to do this, it’s good to just get your feet wet and start modelling. Create one model, or more. Unleash your creativity. What you gain is insight. The model will improve as skill and domain insight improves. The ideas in this article should help you make a start.
- The book by Eric Evans. “Domain-Driven Design: Tackling Complexity in the Heart of Software”. Chapter 14, Maintaining Model Integrity.
- Article by Alberto Brandolini at InfoQ Strategic Domain Driven Design with Context Mapping.