I'm now building Prise — a smart productivity application for founders and freelancers!
Visit Prise for a free 14-day trial (no credit card required!).
Over the last couple of weeks we have looked at a couple of important topics in the world of Domain Driven Design.
First we looked at what are Domain Models and why are they so important to Domain Driven Design. A Domain Model is the focused knowledge around a specific problem to the business.
Next we looked at Bounded Contexts and how they fit into the Context Map of an entire organisation. A Bounded Context is the boundary around a specific Domain Model, whereas a Context Map is the global view of how each individual Bounded Context fits into the bigger picture.
The vast majority of all Domain Driven Design applications will have multiple Bounded Contexts. This could mean integration with third-party services, but it is also often the case that you need to integrate with existing legacy applications or simply other models of the same application.
There are many technical ways to integrate Bounded Contexts, third-party services and legacy applications.
However, choosing the correct integration pattern is extremely important because it will have a big impact on the design of your application and the future of your project as a whole.
In today’s article we’re going to at the strategies for integrating Bounded Contexts in a Domain Driven Design application, the pros and cons of each and how to come to a decision of which to choose for your project.
Recap of Domain Models and Bounded Contexts
Before I jump into talking about the integration strategies, first we’ll do a quick recap of Domain Models and Bounded Contexts.
A Domain Model is the focused knowledge around a particular problem. This includes the Entities, Value Objects and relationships between the things of importance that are specific to that particular problem.
A Bounded Context is the boundary around a Domain Model. The language, names of objects and ideas within the Bounded Context should form a unified model of the problem at hand. The Bounded Context shields the internal model from the complexity of the outside world. Each Bounded Context should have an internal model that is clearly understood by all members of the team. This is important because in most organisations, certain terms will have different meanings across departments or areas of the business.
And Finally the Context Map is the global view of how each Bounded Context fits together within the application or organisation. This bigger picture view of the problem ensures that the goal of the application is not lost in the focused details of each Bounded Context. It’s better to have a true internal model for each Bounded Context with a layer of translation, rather than having monolithic objects that try to fill the role of different, often conflicting jobs.
The reality of software projects
In an ideal world, every software project would start with a blank slate, a clean git repository and no legacy headaches.
However, in the real world, these types of projects are very rare.
The vast majority of all non-trivial application development projects will require multiple Bounded Contexts.
When working with an existing organisation, it is usually the case that you will be required to integrate with legacy applications or third-party services.
It is therefore your job to integrate these legacy applications and third-party systems with the new project you are working on.
The problem with these types of integrations is, there are an unlimited number of situations you can find yourself in. For example, you could find yourself at the mercy of a third-party service, or perhaps you are responsible for providing an interface to an existing legacy system.
There are a number of different integration strategies that I will discuss in this article. Each strategy has it positives and negatives.
Choosing the correct integration strategy for your particular situation is extremely important because it’s going to have a major impact on the design of your application and it’s future development path.
A simple example of translation
Before we jump into discussing the integration strategies, first I will explain a simple example so we’re all on the same page as to why this is important.
Imagine that we’ve been brought into an existing offline retailer as Consultants to create a new online Ecommerce website for the company.
The company has been trading on the high street for a number of years now and so it already has existing stock management, distribution and financial systems.
Our assignment is to create an Ecommerce website that can interface with the company’s existing systems to provide a seamless new channel for sales.
Our new development project is clearly a new Bounded Context within the scope of the Context Map of the Organisation as a whole. We aren’t going to be extending any of the current systems, but we must consume and return data from each of the existing systems in order to make the new channel seamlessly integrate into the current architecture.
We will need to be able to request data from the stock management system in order to know what should be made available to purchase online.
We will also need to send data to the distribution and financial systems so orders are processed correctly and the company’s accountancy liabilities are dealt with.
If this was a real life situation, we would probably have to interface with a whole load of other existing systems and third-party services. However, hopefully you can see the requirement for being able to integrate with existing systems through a layer of translation.
Whilst in a theoretical idea world, it would be wonderful if Ecommerce sales, stock management, distribution and finances were all part of a single unified model. However, the complexity of such a system would quickly get out of control.
Instead we need to find ways of integrating through a layer of translation. The following are 7 ways of doing just that.
The first integration strategy is to use a Shared Kernel, where a part of the Domain Model is shared between different teams working on the same application.
The Shared Kernel integration strategy is beneficial because it reduces the amount of duplicated code and the overhead of translation layers.
However a Shared Kernel will only work if all of the development teams work to share and communicate the requirements of the shared code. This means that the design of the Shared Kernel can’t evolve as quickly as other aspects of the application, and any changes must be agreed upon by members of each development team.
Each development team will also have to take equal responsibility for maintaining unit tests that ensure the functionality of the Shared Kernel remains consistent.
When there is a shared requirement of a certain aspect of the application, and relatively high levels of communication and low levels of political unrest, the Shared Kernel integration strategy can be easier to implement than many of the other integration strategies we will look at in this article.
An example of using a Shared Kernel in our Ecommerce analogy might be that the Customer Model is shared between the Transaction team and the Online Marketing team.
The Transaction team requires the Customer Model to associate transactions and request payments, whereas the online marketing team requires the Customer Model to send marketing information to stimulate new purchases and keep the customer informed of new products and offers.
Instead of both teams writing their own Customer Model and translating back and forth, they could have a shared Customer Model that they both rely on to satisfy their requirements. This means that the teams need to agree on the specification of the Customer Model, and any future changes must be signed off by both teams.
Customer / Supplier
A common relationship between two software applications is where a downstream application requires data from an upstream application, but the upstream application is not dependent on the downstream application.
This relationship can play out in a number of different ways.
Firstly the downstream team can be at the mercy of the upstream team if the upstream team make changes without thinking about the requirements of the downstream team.
Alternatively the upstream team can feel restricted on the design and implementation of their application if the downstream team has control over how their application evolves.
The downstream team is dependent on the upstream team, but the upstream team is not responsible for the deliverables of the downstream team. In order to make this situation work, the two teams need to have a formal relationship where the requirements of the downstream team are considered, as would occur in any Customer / Supplier relationship.
This means future development priorities, tasks and requests need to be agreed upon by members of both teams.
Both teams should also agree upon a series of Acceptance tests that will ensure the interface of the boundary remains consistent. This means the upstream team can evolve their application as long as the interface remains consistent, and the downstream team can be assured that they won’t wake up one day to find their application broken because of the upstream team’s changes.
The Customer / Supplier relationship works best when both teams’ goals are aligned or they both report to the same layer of management. When the goals of the two teams are not aligned, the relationship will often break down.
An example of the Customer / Supplier relationship from our Ecommerce analogy could be where a separate Analysis application must take data from the Ecommerce application in order to generate customer recommendations and predict new trends.
The Analysis application is in a different Bounded Context because it is likely to be written in a different programming language and use very different tools and persistent storage than that of the Ecommerce application.
The Analysis application is relying on the Ecommerce application to send data of the transactions, customer profiles and tracking events in order to run the analysis.
The two teams should agree upon on the type, format and method of the data and how it should be transferred downstream. If both teams are aligned in increasing the success and profitability of the company as a whole, this relationship is more likely to work.
When the relationship between the Customer and the Supplier is not mutually aligned, the downstream team can end up in a situation where it is at the mercy of the upstream team.
This can occur in the same company where each team reports to a very different layer of management with different goals, or where the Supplier has many small Customers and so each individual Customer is not particularly important to the Supplier.
This means that the upstream team has no motivation to provide any kind of priority or even consistency to the downstream team. The downstream team just has to accept the fact that they cannot rely on the upstream team and a consistent interface at the boundary of the relationship.
If the value of the upstream application is critically important to the downstream application, the only way to continue is to adhere to the whims of the upstream team.
This will mean that the upstream team will be completely in control of the integration of the two applications and so the downstream will have to just make it work.
This will cause the downstream team to have a deep dependency on the upstream team and so any future development will be constrained by the situation.
An example of the Conformist relationship in our Ecommerce analogy could be where we rely on a third-party delivery service to send packages to our customers. In order to update our customers on the location of their progress we need to request updates from the third-party delivery service.
However, we are only one of thousands of customers of the delivery service, and so they have no motivation to provide the data we require. The delivery service is also free to change their API at any point, potentially breaking our integration.
In this situation we are completely at the mercy of the delivery service and so we have to take responsibility for accepting their data and the interface they provide for requesting it. If the interface changes, it is our responsible to conform to those changes and evolve our integration to meet those requirements.
When working with existing legacy applications or third-party services, the integration requirements can often be complex. Whilst it might initially seem easier to avoid the integration all together, if the other system is critically important, there is probably more value in the integration.
Integrating other systems is difficult because the other model can leak through the integration and begin to affect the new system’s own model. By adapting to the existing systems too much, we can end up with a new system that has an inconsistent or unsuitable model to solve it’s own problem.
In order to prevent the model from an external system leaking internally to a new system, we need a way to translate the data between the two models. This will often mean ensuring the context of how the data is interpreted is consistent in the new model, but also preventing unnecessary data from leaking through the integration.
To achieve this we need to create an isolating layer that can communicate with the existing interfaces of the legacy and third-party systems, and then translate that back forth between our internal model as requests come and go.
An example of using an Anti-Corruption Layer in our Ecommerce analogy could be linking an existing customer loyalty scheme from the offline retail system with a customer loyalty scheme for our new online retail system.
If an existing offline customer has an existing loyalty balance with the retailer and she starts to shop online, those two systems should know about each other so the customer does not lose out on benefits or discounts.
However the two systems are likely going to have different models and different supporting data that is not relevant between the two systems.
In order to keep the two systems consistent we can set up Web Hooks (What are Webhooks?) that will send a request whenever the customer loyalty balance is updated on one side of the relationship.
When a request is sent or received, it will travel through the Anti-Corruption Layer to be translated into the appropriate form for the receiving application. This means data can flow between the two systems without having to change the existing offline system and without having to conform the new system to the model of the existing system.
When your application will need to integrate with another system, you will typically provide a layer of translation to make this integration easier.
However when your application needs to integrate with many other existing systems, having all of these layers of translation can start to become unwieldily.
Instead of providing one off translation layers for each integration, instead provide a set of Services that can be consumed by any other Bounded Context.
By providing your application as a series of Services you reduce the overhead required to maintain multiple layers of translation.
These Services are likely to evolve as new functionality is created internally, or requested from external consumers. However for one off integration requirements, a single layer of translation will be better than compromising the generic Service interface.
An example of using the Open Host strategy in our Ecommerce application could be providing data about the customers and transaction as a series of Services.
Many of the existing applications inside the company will likely need to request data from our application in order to fulfil orders or restock products, and so instead of providing layers of translation for each system, we can provide a set of Services to reduce this overhead.
Integrating the model from an external system into a new system is difficult because you don’t want your new system to be influenced by the design of the external system.
The external system’s model is likely going to be incompatible with your internal model and so accepting their model as a data exchange language can mean your model is dependent and influenced by the external system.
Instead of relying on the model as a data exchange language, we should use common published languages such as JSON or XML that allow data to be translated between different systems using a common format.
An example of using a Published Language in our Ecommerce analogy could be updating an Messaging application whenever a new transaction occurs. The Messaging application will send emails to the customer to notify them of product updates, discounts and related products.
We can ensure that the data is compatible with the Messaging application by sending details of the request as JSON or XML. By sending the data as a common format we aren’t forcing the Messaging system to conform to our internal model or make compromises in how they design their internal application.
In many situations, integration can be more costly than it is worth. This could be due a Conformist situation, or perhaps the other team is just too difficult to work with.
If the functionality or data between the two system is not inherently linked, just because it is related, does not mean that it should be integrated.
Instead of trying to force an integration, allowing two systems to go their Separate Ways is another attractive option.
An example of Separate Ways in our Ecommerce analogy could be that of reporting financial statistic to the retailer’s existing finance department. The finance department uses an archaic system that would take a long time to integrate with our new ecommerce application.
The Financial liabilities of the company need to be reported weekly, monthly, quarterly and yearly. Investing time to build integration between the two systems for real-time reporting is superfluous to our needs.
Instead we can simply ensure that financial reports can be exported in the required format. This means the two systems don’t need to be integrated at all and so we lose the overhead of trying to make the integration work.
It’s almost inevitable that you will need to integrate with existing applications, third-party services or multiple Bounded Context in any non-trivial application.
Many different types of companies around the world can reap huge productivity benefits from integrating new and existing systems within their organisation.
Understanding the common patterns around integration will be a huge asset when you are tasked to integrate two very different systems.
Integration is also something important to keep in mind when developing a totally new application. By providing your application as a series of services, you will make any future integration requirements with your application a whole lot easier!
The power of software is really magnified when distributed systems can be integrated and leveraged as a whole. Understanding how to integrate applications under different circumstances is very valuable knowledge to possess.
I'm now building Prise — a smart productivity application for founders and freelancers!
Visit Prise for a free 14-day trial (no credit card required!).