After the rant about layered architecture in the previous chapter, you are right to expect this chapter to discuss an alternative approach. We will start by discussing the Single Responsibility Principle and the Dependency Inversion Principle. They are the "S" and the "D" of the SOLID principles, which you can read about in detail in "Clean Architecture" by Robert C. Martin or on Wikipedia at https://en.wikipedia.org/wiki/SOLID).
Everyone in software development probably knows the Single Responsibility Principle (SRP) or at least assumes they know it.
A common interpretation of this principle is this:
A component should do only one thing, and do it right.
That's good advice, but not the actual intention of the SRP.
"Doing only one thing" is actually the most obvious interpretation of a single responsibility, so it's no wonder that the SRP is frequently interpreted like this. Let's just note that the name of the SRP is misleading.
Here's the actual definition of the SRP:
A component should have only one reason to change.
As we can see, "responsibility" should actually be translated to "reason to change" instead of "do only one thing."
Perhaps we should rename the SRP the "Single Reason to Change Principle."
If a component has only one reason to change, it might end up doing only one thing, but the more important part is that it has only this one reason to change.
What does that mean for our architecture?
If a component has only one reason to change, we don't have to worry about this component at all if we change the software for any other reason, because we know that it will still work as expected.
Sadly, it's very easy for a reason to change to propagate through code via the dependencies of a component to other components; see the following figure:
Figure 2.1: Each dependency of a component is a possible reason to change this component, even if it is only a transitive dependency (dashed arrows)
In the preceding figure, component A depends on many other components (either directly or transitively) while component E has no dependencies at all.
The only reason to change component E is when the functionality of E must change due to a new requirement. Component A, however, might have to change when any of the other components change because it depends on them.
Many codebases grow harder – and thus more expensive – to change over time because the SRP is violated. Over time, components collect more and more reasons to change. After having collected many reasons to change, changing one component might cause another component to fail.
I once was part of a project where my team inherited a 10-year-old code base built by another software shop. The client had decided to replace the development team to make maintenance and development better and less expensive in the future.
As was to be expected, it was not easy to gain an understanding of what the code actually did, and changes we made in one area of the codebase often had side effects in other areas. But we managed – by testing exhaustively, adding automated tests, and refactoring a lot.
After some time of successfully maintaining and extending the code base, the client requested a new feature to be implemented in a way that struck me as very awkward for the users of the software. So, I proposed to do it in a more user-friendly way that was even less expensive to implement since it needed fewer overall changes. It needed a small change in a certain very central component, however.
The client declined and ordered the more awkward and expensive solution. When I asked the reason for this, they said that they were afraid of side effects because changes in that one component by the previous development team had always broken something else in the past.
Sadly, this is an example of how you can train your client to pay extra for modifying badly architected software. Luckily, most clients will not play along with this game, so let's try to build good software instead.
In our layered architecture, the cross-layer dependencies always point downward, to the next layer. When we apply the SRP on a high level, we notice that the upper layers have more reasons to change than the lower layers.
Thus, due to the domain layer's dependency on the persistence layer, each change in the persistence layer potentially requires a change in the domain layer. But the domain code is the most important code in our application. We don't want to have to change it when something changes in the persistence code.
So, how can we get rid of this dependency?
The Dependency Inversion Principle (DIP) provides the answer.
In contrast to the SRP, the DIP means what the name suggests:
We can turn around (invert) the direction of any dependency within our codebase.
Actually, we can only invert dependencies when we have control over the code on both sides of the dependency. If we have a dependency on a third-party library, we cannot invert it, since we don't control the code of that library.
How does that work? Let's try to invert the dependency between our domain and persistence code so that the persistence code depends on the domain code, reducing the number of reasons to change the domain code.
We start with a structure such as the one in Figure 1.2 from Chapter 1, What's Wrong with Layers? We have a service in the domain layer that works with entities and repositories from the persistence layer.
First of all, we want to pull up the entities into the domain layer because they represent our domain objects and our domain code pretty much revolves around changing state in those entities.
But now, we will have a circular dependency between both layers since the repository from the persistence layer depends on the entity, which is now in the domain layer. This is where we apply the DIP. We create an interface for the repository in the domain layer and let the actual repository in the persistence layer implement it. The result is something like what you see in the following figure:
Figure 2.2: By introducing an interface in the domain layer, we can invert the dependency so that the persistence layer depends on the domain layer
With this trick, we have liberated our domain logic from the oppressive dependency on the persistence code. This is a core feature of the two architecture styles we are going to discuss in the upcoming sections.
Robert C. Martin cemented the term "clean architecture" in his book of the same name (Clean Architecture by Robert C. Martin, Prentice Hall, 2017, Chapter 22). In clean architecture, in his opinion, the business rules are testable by design and independent of frameworks, databases, UI technologies, and other external applications or interfaces.
That means that the domain code must not have any outward-facing dependencies. Instead, with the help of the DIP, all dependencies point toward the domain code.
The following figure shows how such an architecture might look on an abstract level:
Figure 2.3: In a clean architecture, all dependencies point inward toward the domain logic. Source: "Clean Architecture" by Robert C. Martin
The layers in this architecture are wrapped around each other in concentric circles. The main rule in such an architecture is the dependency rule, which states that all dependencies between those layers must point inward.
The core of the architecture contains the domain entities, which are accessed by the surrounding use cases. The use cases are what we have called services earlier but are more fine-grained to have a single responsibility (that is, a single reason to change), thus avoiding the problem of broad services that we discussed earlier.
Around this core, we can find all the other components of our application that support the business rules. This support can mean providing persistence or providing a UI, for example. Also, the outer layers may provide adapters to any other third-party component.
Since the domain code knows nothing about which persistence or UI framework is used, it cannot contain any code specific to those frameworks and will concentrate on the business rules. We have all the freedom we could wish for to model the domain code. We could, for example, apply Domain-Driven Design (DDD) in its purest form. Not having to think about persistence or UI-specific problems makes that so much easier.
As we might expect, clean architecture comes at a cost. Since the domain layer is completely decoupled from the outer layers, such as persistence and UI, we have to maintain a model of our application's entities in each of the layers.
Let's assume, for instance, that we are using an object-relational mapping (ORM) framework in our persistence layer. An ORM framework usually expects specific entity classes that contain metadata describing the database structure and the mapping of object fields to database columns. Since the domain layer doesn't know the persistence layer, we cannot use the same entity classes in the domain layer and have to create them in both layers. That means we have to translate between both representations when the domain layer sends and receives data to and from the persistence layer. The same translation applies between the domain layer and other outer layers.
But that's a good thing. This decoupling is exactly what we wanted to achieve to free the domain code from framework-specific problems. The Java Persistence API (the standard ORM-API in the Java world), for instance, requires ORM-managed entities to have a default constructor without arguments that we might want to avoid in our domain model. In Chapter 8, Mapping between Boundaries, we will talk about different mapping strategies, including a "no-mapping" strategy that just accepts the coupling between the domain and persistence layers.
Since the clean architecture by Robert C. Martin is somewhat abstract, let's go a level of detail deeper and look at a "hexagonal architecture," which gives the clean architecture principles a more concrete shape.
The term "hexagonal architecture" stems from Alistair Cockburn and has been around for quite some time (The primary source for the term "Hexagonal Architecture" is Alistair Cockburn's blog post at https://alistair.cockburn.us/hexagonal-architecture/). It applies the same principles that Robert C. Martin later described in more general terms in his clean architecture:
Figure 2.4: A hexagonal architecture is also called a "ports-and-adapters" architecture since the application core provides specific ports for each adapter to interact with
The preceding figure shows what a hexagonal architecture might look like. The application core is represented as a hexagon, giving this architecture style its name. The hexagon shape has no meaning, however, so we might just as well draw an octagon and call it "octagonal architecture." According to legend, the hexagon was simply used instead of the common rectangle to show that an application can have more than four sides connecting it to other systems or adapters.
Within the hexagon, we find our domain entities and the use cases that work with them. Note that the hexagon has no outgoing dependencies, so the dependency rule from Martin's clean architecture holds true. Instead, all dependencies point toward the center.
Outside of the hexagon, we find various adapters that interact with the application. There might be a web adapter that interacts with a web browser, some adapters interacting with external systems, and an adapter that interacts with a database.
The adapters on the left-hand side are adapters that drive our application (because they call our application core) while the adapters on the right-hand side are driven by our application (because they are called by our application core).
To allow communication between the application core and the adapters, the application core provides specific ports. For driving adapters, such a port might be an interface that is implemented by one of the use case classes in the core and called by the adapter. For a driven adapter, it might be an interface that is implemented by the adapter and called by the core.
Due to its central concepts, this architecture style is also known as a "ports-and-adapters" architecture. Just like clean architecture, we can organize this hexagonal architecture into layers. The outermost layer consists of the adapters that translate between the application and other systems. Next, we can combine the ports and use case implementations to form the application layer, because they define the interface of our application. The final layer contains the domain entities.
In the next chapter, we will discuss a way to organize such an architecture in code.
Call it clean architecture, hexagonal architecture, or ports-and-adapters architecture – by inverting our dependencies so that the domain code has no dependencies to the outside, we can decouple our domain logic from all those persistence and UI-specific problems and reduce the number of reasons to make changes throughout the codebase. And fewer reasons to change means better maintainability.
The domain code is free to be modeled as best fits the business problems while the persistence and UI code is free to be modeled as best fits the persistence and UI problems.
In the rest of this book, we will apply the hexagonal architecture style to a web application. We'll start by creating the package structure of our application and discussing the role of dependency injection.