Chapter 4. Context Mapping
Not only does the bounded context pattern protect the consistency of a ubiquitous language, but it also enables modeling. You cannot build a model without specifying its purpose—its boundary. The boundary divides the responsibility of languages. A language in one bounded context can model the business domain for the solving of a particular problem. Another bounded context can represent the same business entities, but model them for solving a different problem.
Moreover, models in different bounded contexts can be evolved and implemented independently. That said, bounded contexts are not independent. A system cannot be built out of independent components; the components have to interact with one another to achieve the overarching system goals. The same goes for bounded contexts. Although their implementations can evolve independently, they have to integrate with each other. As a result, there will always be touchpoints between bounded contexts. These are called contracts.
The need for contracts results from differences in bounded contexts’ models and languages. Since each contract affects more than one party, they need to be defined and coordinated. Also, by definition, two bounded contexts are using different ubiquitous languages. Which language will be used for integration purposes? These integration concerns should be addressed by the solution’s design.
In this chapter, you will learn about domain-driven design’s patterns for defining relationships and integrations between bounded contexts. These patterns are driven by the nature of collaboration between teams working on bounded contexts. We will divide the patterns into three groups, each representing a type of team collaboration: cooperation, customer–supplier, and separate ways.
Cooperation patterns relate to bounded contexts implemented by teams with well-established communication.
Naturally, this requirement is fulfilled for bounded contexts that are implemented by the same team. It also applies to teams that have dependent goals, where the success of one team depends on the other’s and vice versa. Again, the main criterion here is the quality of the teams’ communication and collaboration.
Let’s take a look at two DDD patterns suitable for cooperating teams: the partnership and shared kernel patterns.
In the partnership model, the integration between bounded contexts is coordinated in an ad hoc manner. A team can notify a second one about a change in the API, and the second team will cooperate and adapt, as shown in Figure 4-1. No drama or conflicts.
The coordination of integration here is two-way. No one team dictates the language that is used for defining the contracts. The teams can work out the differences, and choose the most appropriate solution. Also, both sides cooperate in solving any integration issues that might come up. Neither team is interested in blocking the other one.
Well-established collaboration practices, high levels of commitment, and frequent synchronizations between teams are all required for successful integration in this manner.
Note that this pattern might not be a good fit for geographically distributed teams, since it may present synchronization and communication challenges.
The second group of collaboration patterns we’ll examine are the customer–supplier patterns. As shown in Figure 4-3, here one of the bounded contexts—the supplier—provides a service for its customers. The service provider is “upstream,” and the customer or consumer is “downstream.”
Unlike in the cooperation case, both teams (upstream and downstream) can succeed independently. Hence, in most cases, we have an imbalance of power: either the upstream or the downstream team can dictate the integration contract.
In this section, we will discuss three patterns addressing such power differences: the conformist, anticorruption layer, and open-host service patterns.
In some cases the balance of power is in favor of the upstream team, which has no real motivation to support its clients’ needs. Instead, it just provides the integration contract, defined according to its own model—take it or leave it. Such power imbalances can be caused by integration with service providers that are external to the organization, or simply by organizational politics.
If the downstream team can accept the upstream team’s model, the relationship between the bounded contexts is called conformist. The downstream team conforms to the upstream team’s model, as shown in Figure 4-4.
The downstream team’s decision to give up some of its autonomy can be justified in multiple ways. For example, the contract exposed by the upstream team may be an industry-standard, well-established model, or it may just be good enough for the downstream team’s needs.
The next pattern addresses the case where a consumer is not willing to accept the supplier’s model.
As in the case of the conformist pattern, the balance of power in this relationship is still skewed toward the upstream service. However, in this case the downstream bounded context is not willing to conform. What it can do instead is translate the upstream bounded context’s model into a model tailored to its own needs via an anticorruption layer, as shown in Figure 4-5.
The anticorruption layer pattern addresses scenarios in which it is not desirable or worth the effort to conform to the supplier’s model, such as:
When the downstream bounded context contains a core subdomain. A core subdomain’s model requires extra attention, and adhering to the supplier’s model might impede the modeling of the problem domain.
When the upstream model is bad or inconvenient. If a bounded context conforms to a mess, it risks becoming a mess itself. This is often the case with integration with legacy systems.
When the supplier’s contract changes often, and the consumer wants to protect its model from such frequent changes. With an anticorruption layer, the changes in the supplier’s model only affect the translation mechanism.
From the modeling perspective, the translation of the supplier’s model isolates the downstream consumer from foreign concepts that are not relevant to its bounded context. Hence, it simplifies the consumer’s ubiquitous language and model.
This pattern addresses the case where the power is skewed toward the consumers. The supplier is interested in protecting its consumers and providing the best service possible.
To protect the consumers from changes in its implementation, the upstream supplier decouples its implementation model from the public interface. This decoupling allows the supplier to evolve its implementation and public models at different rates, as shown in Figure 4-6.
The supplier’s public interface is not intended to conform to its ubiquitous language. Instead, it is intended to expose a protocol convenient for the consumers, expressed in an integration-oriented language. Hence, the public protocol is called the “published language.”
In a sense, the open-host service pattern is a reversal of the anticorruption layer pattern: instead of the consumer, the supplier implements the translation of its internal model.
The last collaboration option is, of course, not to collaborate at all. This pattern can arise for different reasons, in cases where the teams are not willing or able to collaborate. We’ll look at a few of them here.
A common reason for avoiding collaboration is communication difficulties driven by the organization’s size or internal political issues. When teams have a hard time collaborating and agreeing, it may be more cost-effective for them to go their separate ways and duplicate functionality in multiple bounded contexts.
The nature of the duplicated subdomain can also be a reason for teams to go their separate ways. More specifically, when the subdomain in question is generic, if the generic solution is easy to integrate it may be more cost-effective to integrate it in each of the bounded contexts locally. An example is a logging framework; it would make little sense for one of the bounded contexts to expose it as a service, as the added complexity of integrating such a solution would outweigh the benefit of not duplicating the functionality in multiple contexts. Duplicating the functionality would be less expensive than collaboration.
The difference in bounded contexts’ models can also be a reason to go separate ways. The models may be so different that a conformist relationship is not possible, and implementing an anticorruption layer would be more expensive than duplicating the functionality. In such a case, again, it’s more cost-effective for the teams to go their separate ways.
When to Avoid
The separate ways pattern should be avoided when integrating core subdomains. Duplicating the implementation of such subdomains would defy the company’s strategy to implement them in the most effective and optimized way.
After analyzing the integration patterns between a system’s bounded contexts, we can plot them on a context map, as shown in Figure 4-7.
The context map is a visual representation of the system’s bounded contexts and integrations between them. This visual notation gives valuable strategic insight on multiple levels:
- High-level design
A context map provides an overview of the system’s components and the models they implement.
- Communication patterns
A context map depicts the communication patterns between teams—for example, which teams are collaborating and which prefer “less intimate” integration patterns, such as the anticorruption layer and separate ways patterns.
- Organizational issues
Finally, a context map can give an insight into organizational issues. For example, what does it mean if a certain upstream team’s downstream consumers all resort to implementing an anticorruption layer, or if all implementations of the separate ways pattern are concentrated around the same team?
Bounded contexts are not independent. They have to interact with each other. The following patterns define different ways bounded contexts can be integrated:
Bounded contexts are integrated in an ad hoc manner.
- Shared kernel
The integration contract is defined as a compiled library that is referenced by both bounded contexts.
The consumer conforms to the service provider’s model.
- Anticorruption layer
The consumer translates the service provider’s model into a model that fits the consumer’s needs.
- Open-host service
The service provider implements a published language—a model optimized for its consumers’ needs.
- Separate ways
It’s less expensive to duplicate particular functionality than to collaborate and integrate it.
The integrations between bounded contexts can be plotted on a context map. This tool gives insight into the system’s high-level design, communication patterns, and organizational issues.
Now that you have learned about domain-driven design’s tools for analyzing and modeling business domains, let’s take a look at the different patterns for implementing these models in code.