There are several ways to design a system in software engineering, and every design has its own merits and challenges. So metimes different design approaches try to achieve similar objectives. When we think about software architecture design, especially in the object-oriented world, the three most talked about patterns are Clean Architecture, Hexagonal Architecture, and Onion Architecture.
When I observe these patterns, I feel all three patterns are trying to advocate similar ideas. They all define a loosely coupled testable system that avoids any direct dependencies in terms of implementation, yet do so using their own terminology and each with specific nuances. They all suggest approaches to make software architectures more manageable and testable, but do so in their own way.
If we look at them all together, they offer some useful architectural takeaways that are applicable regardless of the design approach you choose to use. We’ll explore them shortly, but first let’s have a look at what each of these patterns are about individually and how they compare to one another.
Hexagonal Architecture is sometimes referred to as ports and adapters architecture. Alistair Cockburn introduced it in 2005, with the core idea behind it being to make applications independent of direct dependency from UI and database. This isolation is supported through the concept of ports and adapters.
Let's take an example. As a developer, you need to design a user related business logic, which will persist in a database. You want isolation between business logic and persistence so that both can perform and grow into their core responsibilities.
A database-specific logic will be wrapped into an adapter class, for example, UserDataAdapter
User specific business logic class, such as “User”
A contract between User and UserDataAdapter so that they can interact with each other — eg.: IUserDataPort. This contact is a port.
As Cockburn explains, the word “hexagon” was chosen not because the number six is important, but rather to allow the people designing the architecture to have enough room to insert ports and adapters as required, ensuring they aren’t constrained by a one-dimensional layered drawing.
Jeffrey Palermo introduced the concept of Onion Architecture in 2008. He wanted to develop a design approach for complex business applications by emphasizing the separation of concerns throughout the system. This pattern took important steps beyond hexagonal architecture as it expanded on the idea of defining a Core Business Layer within an application and the various layers surrounding it, so that the core layer is independent of outer layers and their dependencies.
The central layer — the domain model — contains all business rules. At the next level are domain services, which are like contracts of repositories and other dependencies. The outermost layer contains the user interface and connectivity to external infrastructure.
In short, the key difference between onion architecture and hexagonal architecture is that onion architecture introduces different layers, along with the core business layer, in the application and moves connections to external dependencies such as databases and UI to the outer circle. This means they can be more easily replaced if needed.
Robert Martin introduced Clean Architecture in 2012. The core concepts are similar to Onion Architecture, but it has a slightly different terminology. Here, the domain model is referred to as an “entity”. Entity contains business-specific rules and logic, while the application operation specific logic sits in the use case. These use cases orchestrate operations on top of entities to direct them to execute their business rules to achieve the goals of the use case.
At first glance, Clean Architecture provides a better understanding of boundaries and provides a clearer separation of concerns compared to Onion Architecture. They are very closely related and advocate similar ideas, but with different layers. Clean architecture makes it distinctly clear why each layer exists and what their respective responsibilities are. That’s why it’s also known as screaming architecture — it makes everything explicit.
What do architectural patterns teach us?
As we have seen, all three architectural styles share the principles of loose coupling and attempt to minimize moving parts by properly layering the application.
What, then, are the key takeaways that these three patterns offer us? What fundamental architectural principles should we bear in mind?
Centralized business rules
Putting business-specific rules in a centralized place is something suggested by both Clean and Onion Architecture. Although they use different names for very similar concepts, they both encourage us to think about business logic in the same way.
Application specific rules
Again, both Clean and Onion Architecture point in similar directions; they suggest that there should be a layer where you manage application specific logic sitting next to enterprise rules.
This layer will contain operation-specific orchestration and related logic for the application.
All three patterns are aligned on this principle; it emphasizes that source code dependencies should only point inward. The outer layer can only refer to the inner layer and not vice versa.
Isolation between different layers
All three patterns strongly advocate that different parts of the application should be able to grow in isolation with each other and that there should be proper abstraction between each layer of the application.
Most importantly, the core business rules should be independent of:
How you persist it:
Your choice of databases should not affect the core domain
If you switch the type of database, e.g.: SQL to NoSQL, there should not be any change in your business logic.
Interactions between domain and persistence will follow a defined standard and will be independent of persistence details.
How you expose it:
UI logic and use cases should not ever motivate you to alter the core domain.
Whether you expose it via JSON, XML, or GraphQL, the core should not be affected.
Which framework you are using:
Ideally, the core domain should be independent of the framework being used. This may not be very straightforward, but it can be achieved through careful abstractions.
As an example,. if you change from Springboot to Micronaut in Java, Zin to Martini in Golang, WebAPI to Nancy in .NETCore, there should be no change in terms of how you define the core domain.
What your external dependencies are:
The core domain should not be affected with infrastructure and related dependencies. For example, if you’re using AWS Kinesis and you need to replace it with Kafka streams, the core domain should be not at all affected.
Email, SMS, and Events are a few examples of such dependencies
Architecture patterns are the core of how we design our applications. Although we may choose different patterns, by identifying and recognising their similarities, we can follow some fundamental principles that will provide a solid foundation to design a business critical application. In my experience, understanding these rules has helped me to create extendable, testable and comprehensive software systems.
Disclaimer: The statements and opinions expressed in this article are those of the author(s) and do not necessarily reflect the positions of Thoughtworks.