Exploring Variations of Implementing Domain Driven Design With The “Ports and Adapters” Pattern, Part 1
Hexagonal Architecture is a key design pattern to use when implementing Domain Driven Design. It enables evolutionary changes, helps to keep test suites fast and reliable, and protects the system against ripple effects caused by technology issues. This series of blog posts explores its origin and benefits, as well as five possible implementations and their individual traits.
0. Before we begin: Why Domain Driven Design?
The idea of separating business logic from the rest of a system isn’t exactly new: Separation of concerns as a design principle has been around at least since the 1970s, as a means of creating modular, composable computer program. And since the business arguably represents a unique concern, which can be clearly distinguished from others, such as security or networking, it is only logical that we should be able to find ways of encapsulating, abstracting, and protecting it in our implementation code.
In Eric Evans’ seminal book “Domain Driven Design” (a.k.a. “The Blue Book”), he adds an important philosophical aspect:
“The part of the software that specifically solves problems from the domain usually constitutes only a small portion of the entire software system, although its importance is disproportionate to its size. To apply our best thinking, we need to be able to look at the elements of our model and see them as a system. We must not be forced to pick them out of a much larger mix of objects, like trying to identify constellations in the night sky. We need to decouple the domain objects from other functions of the system, so we can avoid confusing the domain concepts with other concepts related only to software technology or losing sight of the domain altogether in the mass of the system”
This understanding of domain-related code as the crucial element of solving business problems is the fundamental principle of Domain Driven Design: We strive to think, model and implement essential business logic of a system in a cleanly separated, isolated space – the Domain Model. All other aspects of a software system are supporting extensions to this model, and should be treated as such.
However, any reasonably large software system often contains complex enough business logic to make implementing a single coherent model hard, if not impossible: Different organisational units work with different understandings both of their specialised area of expertise (a.k.a. subdomain), and of the rest of the system. Their mental models of the business are often partial, contain contradictions, misunderstandings and conflicting terminology. People in an organisation simply have a way of making things work despite all of these shortcomings (and sometimes even because of them). And most of the time, they don’t even notice all the small corrections and adjustments they make in the process.
For an executable model, this just won’t do. As Eric Evans notes,
“The goal of the most ambitious enterprise system is a tightly integrated system spanning the entire business. Yet the entire business model for almost any such organisation is too large and complex to manage or even understand as a single unit. The system must be broken into smaller parts, in both concept and implementation.”
And thus we further separate the domain into multiple Bounded Contexts, each represented by its own dedicated Domain Model.
1. The classic approach: Layered architecture
To keep the domain separate from technological concerns, Evans recommends implementing a layered architecture, containing these four layers:
- Presentation layer (UI, view, graphical interactions)
- Application layer (a.k.a. service layer, containing APIs and “jobs”, i.e. use cases)
- Domain layer (a.k.a. business layer)
- Infrastructure layer (persistence, message passing, architectural frameworks)
The layers are stacked onto each other according to how they depend on each other: Each layer must only ever depend on itself, and the layers below. This is intended to allow for modularity and changeability.
Consequently, a reference implementation should look something like this:
Note: Certainly, a presentation layer could be divided into several, possibly overlapping parts (e.g., a web UI and a mobile app, showing different parts of the system to different users). And the infrastructure layer is not homogeneous either, but rather composed of a number of different purpose modules. However, since their structure is governed by other aspects, they are not part of the Bounded Context – and I’ve chosen to show them as a single layer to make the diagram easier to read.
2. What’s wrong with layers?
The Blue Book was written in 2004, and at the time, just about any web based application was written using layered architecture. It was “state of the art” back then, recommended and discussed extensively in Martin Fowler’s highly influential book “Patterns of Enterprise Application Architecture”. It was also not the only way to write applications back then, and it certainly isn’t today.
Eric Evans even says in the book, that sometimes it’s advisable to build what he calls “Smart UI” applications:
”Therefore, when circumstances warrant:
Put all the business logic into the user interface. Chop the application into small functions and implement them as separate user interfaces, embedding the business rules into them. Use a relational database as a shared repository of the data. Use the most automated UI building and visual programming tools available.”
But makes sure to clarify:
”Smart UI is an alternate, mutually exclusive fork in the road, incompatible with the approach of domain-driven design.”
The crucial factors, which should influence the decision whether to apply Domain Driven Design, are 1) the perceived complexity of the problem space (i.e., the business logic), and 2) the technical and analytical maturity of the implementation team: Successful DDD benefits greatly from many advanced software development techniques (such as BDD, TDD, refactoring, and the intentional use of connascence). Moreover, naive attempts at strategic design, without an experienced guide or facilitator, can easily make things worse, instead of better.
But even after the decision is made to go with Domain Driven Design, some aspects of layered architecture still remain a potential problem:
- Dependencies may point downward only, but even downward dependencies remain a liability. For example, in the layered style, a change in the database structure will almost certainly cause redeployment of the entire stack: The business layer depends on it, the application layer depends on the business layer and the database, the GUI depends on the application layer – we call this a “ripple effect.”
- Because the database is at the bottom of the stack, and changing it has major consequences (not the least of which are significant costs), we tend to design it first, and protect its design from changing too often. Thus, data modelling — especially for relational data — inadvertently shapes and informs (sometimes unconsciously) our thinking about the structure of business objects. This can ultimately cause the design of said business objects to mimic the design of a normalised relational database, in the worst case. It may lead to inferior, sometimes even harmful architecture decisions, which negatively impact the entire system. Some examples are the partitioning of distributed systems by data ownership, which leads to the “Distributed Monolith” anti-pattern, and the dreaded “Canonical Data Model.”
- Once the decision for a particular database technology is made, it is very hard to replace. While object-relational mappers, such as Hibernate, may allow us to swap an in-memory database (h2) for the production db (MariaDB cluster) during testing, a running database is still necessary — and tests become significantly slower. Switching the technology model used for persistence in production, then, becomes a major undertaking that can take weeks, months, or even years to achieve. And this may become necessary sooner than one would think, due to changed performance requirements, network restrictions, or regulatory changes — be it from SQL to NoSQL, or to in-memory- or file-based storage.
3. Enter Hexagonal Architecture
Shortly after the publication of the Blue Book, in 2005, Alistair Cockburn published the original version of what he called the Hexagonal Architecture, a.k.a. the Ports and Adapters pattern. Alistair had previously published a book on Writing Effective Use Cases, and had his own issues with finding business code scattered across the system – and with the effects of layered architecture:
“One of the great bugaboos of software applications over the years has been infiltration of business logic into the user interface code. The problem this causes is threefold:
- First, the system can’t neatly be tested with automated test suites because part of the logic needing to be tested is dependent on oft-changing visual details such as field size and button placement;
- For the exact same reason, it becomes impossible to shift from a human-driven use of the system to a batch-run system;
- For still the same reason, it becomes difficult or impossible to allow the program to be driven by another program when that becomes attractive.
The attempted solution, repeated in many organisations, is to create a new layer in the architecture, with the promise that this time, really and truly, no business logic will be put into the new layer. However, having no mechanism to detect when a violation of that promise occurs, the organisation finds a few years later that the new layer is cluttered with business logic and the old problem has reappeared.”
He also found a symmetry between the GUI and another part of the application, which corresponds to the infrastructure layer:
”An interesting similar problem exists on what is normally considered “the other side” of the application, where the application logic gets tied to an external database or other service. When the database server goes down or undergoes significant rework or replacement, the programmers can’t work because their work is tied to the presence of the database. This causes delay costs and often bad feelings between the people.
It is not obvious that the two problems are related, but there is a symmetry between them that shows up in the nature of the solution.”
Alistair’s solution is as simple as it is radical: He proposes to forego the concept of layers entirely, and instead expose the core of the application — the business — to all external concerns via “ports”, i.e. a public API fit to each individual purpose. Any number of “adapters” can connect to these, and work seamlessly as a whole, leaving the core itself oblivious to technical details and the exact nature of the external parts. This logic can be applied both to what he calls the “left side” of the application — the “driving”, or event handling ports used by the GUI, batch mechanisms or remotely connected controlling applications, such as ERP systems, and also to the “right side”, i.e. the “driven” ports, used for connecting persistence, messaging and remotely controlled applications.
A reference implementation could be drawn like this:
Note: The external adapters depend on protocols/interfaces provided by the core. On the left side, the interface is used by the driving adapters, on the right side, the interface is implemented by the driven adapters. All dependencies point inward, such that the core itself can remain entirely oblivious of its surroundings.
Even though Alistair probably didn’t have Domain Driven Design in mind, when he came up with the pattern (it isn’t mentioned in the article), there can be little doubt it is a perfect fit: It preserves the original intention of separation of concerns — moreover, it enforces an even stricter separation of the business than the layered approach — and it solves all the issues with layered architecture listed in the previous section. Moreover, its technology-agnostic nature also allows it to be implemented in many ways, providing a variety of options, according to the scale and monolithic/distributed nature of the intended product.
In the remainder of this series of blog posts, we will explore and compare some possible implementations and their individual advantages and drawbacks.
4. The scenario: A web shop with ambitions
How does one demonstrate the effects of a large scale implementation pattern, which shows its true value over time?
My answer is: By showing several stages of an evolutionary change, from simple to complicated, from monolithic to distributed, from relational data storage to event sourcing. Changes that would naturally occur in an uprising business, when you transition from “let’s just get it to work” to “we can’t keep up with the demand, and we urgently need more insights into our users’ wants and needs, to keep our product portfolio attractive.”
The scenario is one we should all be fairly familiar with: Our small startup has an online shop with a catalog of products, from which a user can add items to a shopping cart (or remove them), which is eventually taken to checkout and becomes an order. Luckily, the shop has been gaining a lot of traction, and we want to make it scale massively, soon. For now, we have two frontend applications: A shop GUI, for customers, and an admin GUI for managing the products and keeping track of open carts and orders.
We have thought about licensing an ERP system, which might replace our product management application with a standard logistics platform, and likewise, we might connect to an external service to give us GTIN product codes. But at this time, these are just ideas to keep in mind.
We have done an EventStorming workshop, and this is the output we created:
The domain really has only one bounded context deserving the title, “Shopping Cart”, along with two adjacent contexts, which merely use CRUD logic: “Products”, basically the source of master data, and “Orders”, the bucket in which we store our end product. There is only one transition across the context boundary, wherein a shopping cart is checked out and transformed into an order. There are three business rules (policies), all concerning the lifecycle of shopping carts and orders, and we’ve identified an issue where we want to make sure items are only added to a shopping cart, if they refer to a product that is actually in the catalog (in the past, we’ve had problems where people clicked on cached items in their browser, resulting in orders we couldn’t fulfill due to availability issues).
This domain, and the GUIs, will remain more or less the same throughout all five example implementations – but the structure and deployment of the backend will change significantly, in five steps, from a single-server application to a fully distributed, eventually consistent system:
- A modular monolith: This is our starting point and initial tactical design.
- Shared kernel “microservices”: We keep the domain core as a single library, but change the deployment model.
- Microservices, “for Real”: We split the domain core into share-nothing components.
- CQRS: We divide our APIs into write- and read-related components to allow for better scaling.
- EventSourcing: We replace our relational database with an event store and fully decouple the components by using a message queue.
Of course, the magnitude of this presents a problem: I can either merely show these examples as diagrams, leaving the hard questions about how this can actually work to you, the reader. Or I can provide — reasonably simple, but complex enough to make the principles show — demo implementations, which you can run, test and compare.
I have chosen to do the latter. You will find all the referenced code at Github
In order to be able to do this, I had to omit such “minor” details as security, user and session management, not to mention the actual payment. Please be kind – it should still serve as a decent example.
5. Implementation #1: A Modular Monolith
The first stage in our incremental design evolution is basically how folks should design a properly modularized monolithic application. And to make it clear – this is absolutely a valid design for many contemporary web applications! Monoliths get far too little credit: They usually can be kept running and bug-free at reasonable cost, have a fairly small resource footprint, and their deployment and operations can be easily managed by a single team.
For a very large percentage of businesses — I will go out on a limb here and make a bet — this is absolutely sufficient. Most companies don’t need elaborate microservice architectures and global scale — in fact, none of them do, when they start their businesses: On the contrary, they will greatly benefit from keeping things simple, until demand grows too quickly and it’s no longer enough (which is arguably the best problem to have, when you’re a growing business).
The basic structure can be pictured like this:
Our first implementation contains two frontend apps made with React (shop-gui and admin-gui), and a single server (example-shop), which uses SpringBoot for the backend, and a MySQL database. The domain core is included as a library (domain-core), which does not rely on any external dependencies, except for lombok (to allow for less source code) and joda-money (because we don’t want to implement currency handling ourselves).
This is the key benefit of Hexagonal Architecture: We get to keep our core business entirely framework-agnostic, ensuring that it will only ever have to change when the actual business changes.
This isolation is realized by providing an
api package, which contains interfaces for the incoming (e.g.
OrdersApi) and the outgoing adapters (
OrdersRepository), as well as the serializable data objects (
Order) to be passed to and from the core. The rule is: No outside objects may be used within the core, and we never pass objects containing the business logic (
impl) to external classes. This necessitates some extra mapping (see
ShoppingCartFactory), but prevents the actual use of business methods, and the pollution of our domain with implementation details.
Our business rules have been implemented as domain services (e.g.
OrdersCheckoutPolicyService), which call the APIs internally. and we chose to do the same for our validation problem:
ProductValidationService will throw an error, if an added shopping cart item is not a valid product.
The core was developed using TDD, and is hence fully unit tested. Note that we did not use any mocking tools, but rather implemented a simple
RepositoryInMemory to use during testing. The tests are very fast and give a complete documentation of our business logic.
In the Spring module, we replace our
RepositoryJpa, and within it, we use the well known
CrudRepository from Spring Data. Note that the JPA entity structure is not identical to the business entity structure used within the domain core, and we don’t use lazy loading, or relational annotations, such as
@OneToMany, at all.
We have found that keeping our JPA structures simple, and combining/separating/calculating data using Java streams works well for us, but we may change this in the future.
The finished product is deployed into docker containers behind a load balancer, which makes everything appear, as though it were a single service:
Continued in Part 2.
Special Thanks to Stefan Herrmann and Alexander Rose for helping to sort out both the language and content of this blog post, and for fixing some of my uglier bugs in the example repository 🙂