Introduction
In a software project, classes that contain business logic are often the most important part of the system. We want such classes (I’ll call them domain models or entities) to be easily changeable, quick to understand and unit test. Unfortunately as an application grows, they often become overcomplicated, with multiple responsibilites and hard to read. It is especially cumbersome in a distributed system, when you change a model and you are unsure as to in how many applications it is actually used. This means that you cannot change system components independently, which is the most important asset of the distributed design.
In order to achieve real autonomy of a component, we need it to be isolated from the rest of the system. Its domain model should focus only on business logic and nothing else. In this article I’ll describe how easy it is to lose such isolation and how to protect it.
Firstly, let’s consider situations in which our domain model is poorly isolated.
Database Integration
Databases are often used for data persistence. Modern ORMs automatically bind class members to table column names. It’s very easy to configure database integration like this. See the diagram below.
As we can see, Entity1 class is coupled to a database. This is fine in simple domains, however, in complex ones, this is an additional responsibility and hinders changes of the model as they require database to be changed as well.
Inter-service Communication
There is often a need to exchange data between services. For example, in .NET WCF technology you can easily use your class for communication between services by using [DataContract] and [DataMember] attributes. It might look like this:
Entity2 class is used in communication which means Service2 depends on it. Additionally, the domain model leaks outside the service boundaries. When Entity2 changes, it is not clear if Service2 needs to be updated as well. Clarifying this requires manual investigation.
Backend – Frontend Communication
It often happens that a frontend application makes http calls to backend server to fetch some data. The easiest solution would be to return domain object, so it’s then consumed by a frontend application. See the diagram below.
Entity3 class is used in API call, which means that frontend application depends on it. Again, in this example, our domain model leaks outside the service boundaries. When Entity3 changes, we are uncertain whether the front-end application would be affected.
Lack of Isolation Problems
The consequences of such design are as follows:
- A model can’t be easily changed as other components rely on it (for example, we can’t rename properties)
- Developers are reluctant to refactor the model as they don’t know if the changes don’t cause other components of the system to break
- Such a design often implies that all components need to be deployed at the same time (distributed monolith problem)
- Testing is time-consuming as many components change simultaneously
- There is a high risk of regression errors when a model changes and applications depending on it are not deployed
Protection of Domain Entities
How can we solve the problem? The purpose is not to expose our domain model to the outside world. The solution is to isolate the domain model, so it is only used in the scope of one component and other components have no knowledge of it. To achieve such isolation, we may use the anti-corruption layer pattern. For communication between components, we should use new set of classes called DTOs (Data Transfer Objects). These contain only fields, no methods and are only used to transport data between the system components. It is now clear how and what data is exchanged in the system. Moreover, we simplified the domain model so that it no longer has additional components (like attributes) required for integration with other components. It might look like in the diagram below.
Entity4 is used only in the Component1 and Mapper, so it is properly isolated.
Isolation Benefits
What are the benefits of having a domain model isolated?
- Developers are confident that changes affect only the component in which it is used. You can refactor your model comfortably to keep it up to date with current business requirements
- Changing communication with external component is done explicitly in the mapper, so you always know when the deployment of the other component is needed
- In this design, it is much easier to maintain appropriate encapsulation as you directly see all the places where your model is used
Anti-corruption Layer – Always?
As always, it depends but here are some hints when it should be used:
- At least one component in communication has complex business logic
- Components are developed by different development teams
- Components are to be released independently of each other
Summary
Domain models exist in every software project. It is essential to understand the consequences of how they are used and shared. I hope this article sheds some light on the topic of why and when it is important to isolate them. Introducing DTOs classes in complex domains has some additional costs to it, but it will definitely pay off with time.
How do you feel when you change private method of a class? I feel relaxed, because I know it is highly unlikely that anything will break in some other part of the system. Trust me, you will feel the same when you change your domain model and it is properly isolated.
Author of this blog is Andrzej Stawicki, Aspire Systems.