What is domain-centric architecture?
Based on the XConf 2020 presentation: What is Domain Centric Architecture and why do you need it in your business? Yerko Aguirre — Javiera Laso
The architecture design and development is hard
Conway establishes that system designs are a true reflection of how an organization is structured, if an organization has a silo structure where each team is isolated from the rest, the design and architecture of that organization’s systems will present the same symptoms.
For example, a system that is divided into frontend/backend teams and database administrators where there are silos and communication problems, will produce systems that are isolated and tightly coupled to the context of the team.
This is why we need to set up the business and the teams in a way that represents what is needed in the project. For example, project teams to product teams, and we will see the rationale for this decision as we progress through the article.
Types of architecture in agile development
Software by its nature is changing, when we start an agile project sometimes we do not see or do not care about architecture concepts that are always present, so the product becomes difficult to change in the future (all of us had think at some point: -“we will take care of that later”, and normally that doesn’t happen).
We also know that the architecture is not written in stone and is not rigid, it evolves as the product is developed and it is not trivial to identify where certain business logic should go without a well-defined architecture or a bad architecture.
There are several types of architecture, each seek to solve different problems, and which vary in complexity. We can highlight monoliths and distributed architectures and briefly define them.
Monoliths
Monoliths are those that combine the user interface layer, the business layer and the database in a single system. This means that it is responsible for all the necessary steps for a particular function. In the monoliths we can find:
- Layered architecture
- Modular architectures
- Micro Kernels
Distributed architectures
Distributed architectures are those that, unlike the monolith, their processes are distributed, that is, if we perform a query in the database, we will divide all the processes into different nodes, which will improve the final performance of the application. In this architecture we can find:
- Microservices
- Service-oriented architecture
- Event driven architecture
As we can see, there are several types of architecture, each seek to solve different problems, and which vary in complexity. Among these types there is no concept of the best architecture, since everything “depends” on the context and the problem we want to solve.
Domain driven design
Domain-based design is a domain-centric software language and approach to modeling complex problems. The term was coined by Eric Evans in his book Domain Driven Design. It consists of a collection of patterns, principles, and practices that enable teams to focus on what is critical to business success while building software that manages complexity in technical and business contexts.
In summary, to carry out a good domain-driven design we must follow some steps:
- Alignment is the fundamental part where we must understand the business model (what the company does) and what are the needs of the users.
- By discovering we can take initiatives such as event storming to understand the domains, the contexts and which can be divided for the next process.
- By understanding domains, we can break down into sub domains, and start defining our bounded contexts (which we’ll talk about later), plus
- We must know how to connect the subdomains as a loosely coupled architecture allows teams to work in parallel without being locked out.
- By having a central domain strategy, we can have a common language between the business and the technical side, which helps us define roadmaps and inverse the Conway maneuver.
- When organizing, we must consider that the Conway maneuver is not the best, since it can block us in higher stages.
- By defining roles and responsibilities, we ensure that each bounded context can evolve optimally.
- Use the patterns that best fit your business and start coding!
Common language between business and technology
It is important to maintain a common language between business experts and technology experts, in order to transfer these concepts to the source code, so that our software product reflects the concepts that are commonly spoken by the business.
Examples: Business acronyms must be documented in an accessible place, such as confluence or another wiki that has been created altogether with the involved areas.
Bounded Context
Bounded contexts are the main pattern in Domain-Driven Design. A Bounded Context is a delimited space where a business element has a perfectly defined meaning.
The context is defined by the terms used in talks, in documentation, in meetings, etc.
In the diagram we can see that a product is conceptually completely different according to its context. A product in the shipping context is different from a product in the inventory context. This is where the idea that the meaning varies according to the context gains more strength, but we can also observe that the code could be affected (due to a possible duplication), but we can compare it with the decrease of a high coupling. We also have the freedom to evolve according to the context.
Now that we know the principles of DDD we can talk a bit about the classical architectures and their differences.
Layered v/s domain-oriented architecture
In the layered architecture, the dependency goes from the presentation layer to the persistence layer, which is fine for applications with a low degree of complexity, but as the product grows and its complexity increases, the business rules are grouped to other layers and is difficult to evolve and maintain.
On the other hand, in the domain-centric architecture, the dependencies are inverted and the domain is the center of everything, for which the entities, use cases or business rules do not depend on technology or external agents, therefore which is easier to adapt, to change and evolve.
If we take this to real life we can find the following:
When looking at the project structure above, and focusing on the layered architecture we can try to “guess” without much success what the application does, perhaps we can say: -”It has to do with a veterinary clinic”, but if we look at the one described by a domain-centric architecture, it is more “intuitive” to know what our application is doing, we see for example that although it is about a veterinary clinic, it is also capable of confirming appointments, scheduling new appointments and searching for available veterinarians ( in addition to a number of other things that it does, that can be observed).
We can conclude that the traditional structure breaks the encapsulation, while the domain architecture reinforces it.
From theory to reality: Application in real projects
Hexagonal architecture in an e-commerce
The hexagonal architecture is also known as the architecture of ports and adapters and has the mission of decoupling the layers of the application. It is described as a port, the definition of a public interface and adapter to the specialization of a port for a specific context.
The choice of this architecture is based on its characteristics:
- Framework independent
- Testable
- UI independent
- Database independent
- Independent of external agents
- More tolerant to change
- Reusable
- Maintainable
A popular approach is packaging for technical reasons. But this approach has some drawbacks. Instead, we can package by feature and create self-contained, self-contained packages. The result is a code base that is easier to understand and less prone to errors.
Clean architecture in a real estate company
Clean architecture is a software design philosophy that separates elements of a design into ring levels. The main rule of clean architecture is that code dependencies can only come from the external levels inwards. The code in the inner layers cannot be aware of the functions in the outer layers.
In this case, the choice of this type of architecture is made due to its characteristics:
- Framework and DB independent
- UI can easily change
- Easy testing
- Reusable
- Maintainable
An interesting example of the use of this type of architecture occurs when we try to change a framework like Spring (which has its own complexity) to another without so many functionalities but that adjusts to what we need at that moment for the business, called SparkJava.
Problem: High startup time when using a robust framework like Spring, without needing it. Solution: Change the framework. How this worked: The development was at the framework level with a couple of lines of code that managed to make a “clean” change without much impact to the lower layers. By not having spring security as a library within the framework, it was necessary to implement a middleware, in addition to generating a route configuration file, since spring boot has this functionality incorporated. When testing bootstrapping in a dormant state, we saw that this change decreased the app’s boot time from 21s to 4.4s.
How to get started in domain-centric architecture?
Untangling the monolith, it’s important to see if we can move from a modular monolith to microservices, but not always our monoliths are cleanly described.
The expectation is to have a monolith organized around business capabilities, but the reality is that we have multi-site dependencies with a lot of coupling that is difficult to evolve or change, since a change in one place can affect the behavior of other functionality, which makes the process of changes and extracting business capabilities from the monolith to microservices difficult.
A strategy to avoid dependencies and coupling is the use of modular monoliths where we encapsulate in modules the different bounded context or domains of our application. A strategy to have isolated domains is to achieve communication between modules through an event bus in memory, in this way we managed to avoid coupling between modules, and in the future it is easier to transition to microservices.
Using event storming and bounded context we can define microservices, although we will not always have a 1:1 relationship with respect to microservices/bounded context, and it is possible that in a bounded context there are multiple sub-domains that can be translated into other microservices .
Here it is normal to ask ourselves, what are the principles that draw the limits of the service? Well, there are a couple that we must consider:
Language: Use bounded context to draw boundaries.
Authority: Have autonomy to make decisions.
Size: Divide the service when a team is not able to support that service.
Frequency of changes: Consider grouping concepts with different rates of change, for example, inventory services vs. annual reporting services.
Transactionality: Separate responsibilities of operations vs. reports, i.e: projection of demand vs. historical report of annual sales.
Cohesion: Groups concepts that are related to each other, to ensure that the business experience is maintained.
Consumer Oriented: Organize around business capabilities not around systems.
How do we choose the best? i.e: How do I know if I need a monolith or microservices?
Choose monolith when:
- There is no DevOps culture, it is important that people are able to develop this skill before migrating from the monolith.
- You are prototyping an idea and need to get it to production quickly.
- Existing code is present in a single deployment unit.
Choose microservices when:
- Your team is mature in the business and the company has a DevOps culture.
- Developers on your team have experience developing microservices and understand DDD concepts (sometimes we adopt what’s hot without reading the fundamentals, which can lead to fundamental mistakes that can impact the business).
- When you need independent deployment units and scale out.
Maintain architecture over time
When the project starts, everyone feels comfortable with the architecture, but if we remember today the decisions we made yesterday there may be some significant differences, that is why it is important to give visibility to the decisions we made in the past, to help us guide and verify that the architecture doesn’t drift over time. Here I would like to highlight some practices:
Visualize your architecture using C4 models
The 4 phases of the model are: system context, container diagram, component diagram, and code.
C4 architecture diagrams are models that help explain applications and their systems from the general to the particular. This means that we can explain a system in 5 minutes, in 1 hour or in 5 hours.
As you can see in the example above, we can talk about a cyclist, reaching the molecular structure of rubber, the important thing is to at least have the container or component diagram documented. The question arises: How can we do this documentation?
ADRs
The ADRs or Architectural Decision Records, are the way in which we can document the decisions we make regarding architecture or other issues, these are stored in the source code and there are several ways to write them, a popular structure is presented below:
So, what practical way do we have to maintain our architecture and also deliver value to the business? considering that we already documented in the ADR…
Test your architecture
It is important to have automated tests that ensure that your architecture definition doesn’t drift over time, for example, tests to keep technology layers isolated, for example dependency injection by constructor vs. setter or reflection.
It is important to have automated tests that ensure that your architecture definition doesn’t drift over time like: tests to keep technology layers isolated, for example: dependency injection by constructor vs. setter or reflection.
The main advantages are:
- Avoid cycle-level dependency.
- Prevent the architecture definition from drifting over time.
- Set architecture guidelines in the team.
- Automated tests with technical agreements at team level
In addition to these tests that manage to maintain our architecture, we must consider other ways of testing our application against the unexpected and expected, on the one hand we have our normal development cycle that describes a conventional test pyramid and on the other the fitness functions.
We observe in the image the typical tests that we try to apply in all projects, in development we generally use TDD (in my projects at least) therefore we try to follow the pyramid of tests, where we verify that the characteristics fit the results desired by the business. In fitness functions we can write tests that measure the alignment of a system with the architectural goals.
One example is chaos monkey, created by Netflix and responsible for randomly terminating instances in production to ensure that we developers deploy services to be resilient to instance failures. Some interesting fitness functions that we use and didn’t know they were:
- Code quality: coverage over 90%.
- Resilience Testing: As a test you could run a continuous load on a service while a new version of that service is deployed in a pre-production environment.
- Observability tests: metrics that indicate low conversion or volume of users using splunk (fitness functions based on business metrics).
- Performance tests: For example, when we want a transaction to be carried out in a maximum of 10 seconds, which is a metric defined by experience and the business.
- Security analysis: The typical ones of OWASP (such as avoiding sharing secrets in a deploy to git), but also that there will not be sensitive information in logs, among others.
After all these definitions we see that in addition, fitness functions can be incorporated to monitor the alignment with critical architectural “features”. As the organization and technology evolve, fitness functions can help companies avoid being stuck with outdated architecture or testing that often results in legacy or unnecessary complexity that limits the delivery of business value.
Why do you need domain-centric architecture in your business?
After all the information we have reviewed, we realize that the Conway maneuver in itself is not enough, but that we must create product teams; This way of working is known as the inverse Conway maneuver, where product teams are capable of managing the entire operation of a product by being multidisciplinary, in charge of their own roadmaps but can talk to other products among themselves if necessary.
Another point we learned is that if you take too long to adapt, you give the competition an advantage, so being prepared for change helps us to react quickly. For example, when the covid-19 pandemic hit, many businesses had to adapt their infrastructures to an abnormal amount of online audiences, many failed, but others emerged victorious.
Finally, having a domain-centric architecture gives you a technical and business view. If you climb the mountain and see the city from afar, you can understand the zones in which the city is organized. Making an analogy with domain-centric architecture, in which you know where the business logic is and you understand the language, you have a global vision of how your product is built.
Recommended reading
Here are some books on which this article and presentation were based:
- Fundamentals of software architecture an engineering approach — Mark Richards & Neal Ford
- Domain Driven Design — Erick Evans
- Building evolutionary architectures — Neal Ford, Rebecca Parsons & Patrick Kua
- Monolith to microservices — Sam Newman
I hope this article has been helpful and do not forget to leave your comments and feedback in the box below, the Spanish version can be found here. Thank you very much for reading and reaching the end.