DDD Beyond the Basics: Mastering Multi-Bounded Context Integration

Mario Bittencourt
SSENSE-TECH
Published in
9 min readApr 8, 2022

Domain-Driven Design (DDD) has been around since 2003 when it was introduced by Eric Evans. Because of the potential benefits it can bring to our software development practices and outcomes, I often engage with developers to present its concepts and preach it as a recommended approach, especially for our more complex solutions.

While addressing their questions, or common misconceptions, I found two main recurring topics: integration of multiple bounded contexts and aggregate design. In this article I will cover the integration of multiple bounded contexts. Stay tuned for a follow-up article that will explore the second topic.

I am assuming you have basic knowledge of DDD but if you’re looking for a good base take a look at this post written by my colleague.

It’s About Language

Anyone who has studied DDD has been presented with the many concepts that are used with it: Bounded Context, Entity, Aggregate, Value Object, among others. Some of them, due to their direct code association, are more easily absorbed and applied, while others are relegated due to their abstract nature. Ubiquitous Language is one such case, and despite its simplicity and power can be easily overlooked or forgotten.

The goal is to be able to establish a non-ambiguous way, or language, used to communicate between experts and developers alike. This means that all concepts represented in code or not, such as classes and processes, are supposed to be named not arbitrarily or from a developer-centric standpoint, but instead as the result of converging discussions on how your software solution is trying to solve a given problem.

Perhaps a classic example would be our tendency to refer to the individual that will use the system as a User while the domain expert refers to it as a Customer. Or the inclination to refer to the concepts we manipulate as SomethingData or SomethingInformation.

That seemingly innocent difference is an example of what can lead to confusion or the “broken telephone game” where the intent is lost as it gets translated from what the business expects to happen, to what gets implemented.

Converging on the right language is powerful, as it helps you to make implicit definitions explicit, which is always a good thing. Imagine that instead of calling something Shipping Information you discover that it is indeed a Destination Address.

Now let’s talk about the scope. In a programming language, the scope is easily understood as the area of the program where an item, such as a variable, is recognized. Outside of that scope, there is no guarantee that the same concept exists or even has the same value/meaning. When we bring this discussion to our non-programming language, we are acknowledging that our carefully crafted language has limits. This means it is ok, and even encouraged, to not pursue a universal enterprise-wide definition of a concept that can be used in all its applications.

To define such limits and explore why that is desired, it is necessary to discuss Bounded Contexts.

Let’s Set Some Boundaries

When discussing the concept of Bounded Contexts (BC), Eric Evans stated that we should “Explicitly define the context within which a model applies. Explicitly set boundaries in terms of team organization, usage within specific parts of the application, and physical manifestations such as code bases and database schemas. Keep the model strictly consistent within these bounds, but don’t be distracted or confused by issues outside”.

This statement exposes the fact that there is a limit where our models, and ultimately the language that defines them, are valid. It also reinforces that there are things “outside” the limit that we should not be “distracted” by. While discussing how to define these boundaries falls outside the scope of this article, I want to focus on the mere existence of a limit, which is illustrated in figure 1.

Figure 1. Different Bounded Contexts and their relationships.

In this figure, we note the following characteristics:

  1. Bounded Contexts may or may not have relationships between them
  2. Models can have the same name but different definitions from one Bounded Context to another
  3. Models can have different names from one Bounded Context to another but share some or all common traits

For the Bounded Contexts that do not have any relationship between them, there is nothing much to say other than to reiterate that it is ok if you find the same model name in both of them. Assuming that it was the result of a careful decision/iteration.

On the other hand, for the Bounded Contexts that share a relationship, let’s look at context maps that provide additional information on how their relationship may affect their model and language.

Upstream/Downstream

Figure 2. An upstream/downstream relationship.

Here, the upstream is the one that dictates the relationship, making the downstream adopt the language as part of their own. In our example, the Customer Profile is used, not only in the code, but also recognized as a concept part of the Checkout Bounded Context.

Anti-Corruption Layer

Figure 3. An upstream/downstream with an anti-corruption layer.

In this example, the Warehouse does not need to import the Product definition from a Product Information Management (PIM), but instead defines a local concept that has a subset of the information it needs.

Let’s take a closer look at these examples from an implementation point of view.

Connecting the Dots

Our first implementation will illustrate how an upstream/downstream relationship could be represented. The code and discussion will focus on the integration aspect, more than on the other DDD-related aspects.

Our fictitious Customer Profile defines a model named Ordering Preferences that is used by the Checkout. Imagine that as part of its operation it needs to know the customer’s pre-defined decision to have all its purchases delivered in an expedited manner.

Our application service — the command handler — needs to obtain the ordering preferences for the customer that is attempting to initiate the checkout process. With that information, among other things, a Checkout is created to encapsulate the business rules expected to be followed.

So the OrderingPreferences is a local concept, imported from the Customer Profile and obtained via the OrderingPreferencesService. A common approach is to represent this as an interface.

In our infrastructure layer, we have a concrete implementation of such an interface that actually interacts with the Customer Profile and produces the expected value object.

Here we see that I am making this request using some sort of HTTP client to retrieve the data, which at that point is just a data collection. Then I use that data to create the OrderingPreferences, which is a recognized local concept.

Now let’s look at our second implementation, which aims to illustrate when you are not importing the concept but instead having to construct a local one based on an external definition.

We have a Warehouse operation that, in order to process and ship a customer order, requires us to provide some regulatory information for customs purposes. Contrary to the contextual information received when the order was placed, those extra pieces of information reside in another Bounded Context from a Product Information Management (PIM). In the PIM this information is found as part of a Product model.

Figure 4. Free Trade Agreement made up of portions of the Product definition.

Upon discussion, it was decided that locally there is no need to have the same Product concept and that the required information is known as the Free Trade Agreement.

Like before, we have an application service with its dependencies injected.

The first point to mention is that you are not exposing the PIM’s Product definition to your application. It expects a Free Trade Agreement value object as the output. Like before, one approach is to define an interface with a concrete infrastructure implementation.

The prefix of the TranslatingFreeTradeAgreementService aims at making the role of this service explicit in taking a foreign concept and producing something that follows the local language.

As for the actual retrieval of the information and its manipulation, one recommended approach is to split the responsibilities between an adapter and a translator, with the former making the actual request and the latter taking care of validating and passing along just the necessary information to your value object.

That’s Nice, but Do I Need All of That?

The structure used definitely comes with a lot of moving pieces: the interface, the concrete translating implementation, an adapter, and a translator. Your first reaction may be to think that it is too much or too complex to be useful in real life. In the end, the decision will be up to you and the development ecosystem you adopt, programming language, and tooling.

Here is a brief summary of the purpose of each component and some practical considerations.

  1. The Service Interface

If you know and believe that SOLID principles can foster good practices, the interface helps you to focus on the intent more than being bound to make a decision about too many implementation details upfront.

Some development languages allow you to dynamically replace your existing implementation to facilitate testing, which arguably can make you skip the interface definition.

2. The Translating Service

The goal here is to ensure your translating service knows which service(s) it needs to contact in order to return the local concept your application expects.

It relies on one or more adapters to perform its bidding, acting more like an orchestrator.

3. The Adapter

It is the one that actually uses the infrastructure, http, gRPC, etc. to obtain the data from external Bounded Contexts and pass it to a translator.

If you have a single BC to contact, a potential simplification would be to make the remote call and translation directly from the translating service.

4. The Translator

Takes the data received from the external Bounded Context, validates if it contains the structure we need, and uses it to create the required value object.

No actual business rules should live in the translation. Here you are validating that the data expected to be there is actually there, throwing away what we do not need, performing the minimal transformation in the data if necessary, and passing it to the value object to do the actual domain validation.

Since you will have to write code to perform this manipulation, if you want to avoid the need to create a separate class you can group this functionality as an internal part of the adapter.

Visually this is the relationship between all the parts:

Figure 5. Interface, Translating, Adapter, Translator roles.

My recommendation is to follow a vertical development approach and continuously refactor your code. This functional decomposition will be easier as you advance and acquire more knowledge on the integration that is required.

Conclusion

Integrating multiple Bounded Contexts is more of a norm than an exception, so taking the time to understand the many forms in which these relationships can be maintained is essential. As we saw, it is not “black art” but does require some time to look at how you approach such an integration, which is more than the technical aspects of it.

If you will rely on synchronous or asynchronous communication, REST or gRPC are important and necessary aspects that more often than not should be deferred to when you actually need to make the decision. And, that comes after understanding the type of relationship and the (domain) language that will be adopted.

In the next article, I will cover Aggregate Design, the simple relationship between two more Entities, and when they really become an Aggregate.

Editorial reviews by Catherine Heim & Pablo Martinez.

Want to work with us? Click here to see all open positions at SSENSE!

If you want me to cover any other aspect of DDD please share in the comments section below. ⬇️

--

--