Exploring Variations of Implementing Domain Driven Design With The “Ports and Adapters” Pattern, Part 2
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.
6. Implementation #2: Shared Kernel “Microservices”
In the previous part of this series, we built a first iteration of our web shop, as a modular monolith. It performed well, for the time being. But now that we see more and more traffic on our site, and we’ve reached the limits of adding more CPUs, we have to start thinking about a more distributed setup, with better possibilities for scaling.
Our first step on this journey (find the source code here) is to split our single container backend into separate services. This will enable us to deploy individual parts of the system to entirely separate host machines, and thus achieve significant horizontal scaling: The load balancer, both frontend apps, all three backend service, and even the corresponding database schemata could now each be deployed to a single host, instead of all on the same one — a major level-up in potential computing power.
To make this scenario work, each service must have the highest degree of cohesion (all the moving parts that belong together, from a business capability perspective, are also located closely together), to achieve the smallest degree of coupling (as little back- and forth communication with remote parts of the system, as possible).
We solve this by separating the modules along the outer edges of our previously identified bounded contexts: Our main context
shoppingcart, and the supporting contexts
But let’s not get too far ahead of ourselves: Splitting both the outer (service) and inner (domain-core) parts in one go is quite a long jump without a safety net… What if we only do the outer separation, for now? To keep the important business working as expected, we’ll just preserve the single domain-core for now, and simply import it as a shared library into all the new services.
It turns out, making that transition is not too difficult. We have already put in most of the work: Except for our domain services (those reach across context boundaries), all of our current components are neatly bundled in packages responding to said contexts. We can simply move them to newly added SpringBoot service modules!
Alas, if we just deploy them now, we’ll run into a huge problem: Because, for example, we won’t be able to directly access the
OrdersApi actually being used to store and retrieve orders within
shoppingcart, we must first make sure all our domain services communicate through the network — but keeping the hexagon intact and free from dependencies, we can’t access the network in
domain-core! Doing so would add a dependency, to Spring Boot’s
RestTemplate, for example – and our business would no longer be isolated.
At this point, I’ve seen many teams abandon Hexagonal Architecture; if not in principle, then by allowing hacks and dirty workarounds. Understandable, but unfortunate — by tying our core into popular frameworks, their wiring and structure, we would make it a lot harder to change things and move them around as we please.
Luckily, there is a better solution, and it is fairly simple. It requires only a minor change to
domain-core: We must apply the same reasoning to the domain services that we did to the repositories — they should be ports, providing interfaces, which we can then implement as adapters in our SpringBoot modules.
A diagram of the logical structure of our solution could now look like this:
Using our IDE, we can easily extract these interfaces from our existing
OrdersCheckoutPolicyService, and move them to the
shoppingcart.api package. The original classes remain as
InMemory implementation (in
impl), to keep our test suite running and the business rules verified.
In our newly created SpringBoot
shoppingcart module (we can copy the
Config classes, and remove the parts we don’t need), we can now implement
ProductValidationServiceRest, and so forth, and connect to the respective endpoints of the other services.
Once we’ve adapted our load balancer rules and redeployed the system, it should now look like this:
Now, it should be mentioned again, since it might not be obvious: I don’t recommend pursuing this design as your target architecture. Although it is appealing to keep your domain core in one place, where it’s easy to test and reason about, it also causes the services to be fundamentally coupled – even a minor, localized change in the business will result in a redeployment of all the backend parts, instead of just one.
It is, however, an attractive in-between step, when you’re migrating from monolith to microservices, which you can also nicely combine with the Strangler Fig Pattern when it’s a legacy system.
7. Implementation #3: Microservices, “For Real”
To remove the dependency between our services, and make them fully-qualified microservices, we now need to apply the same separation logic — along bounded context lines — to our domain-core.
This is a slightly more tedious undertaking, because it turns out, there are some objects we used for serialization, which are now no longer available everywhere. All our simple value types:
Fluid must be copied to the
api package of all three core libraries, and the
shoppingcart service requires a copy of
OrderPosition (for JSON in/output in our RESTful domain services).
There is also no longer a use for our
InMemory services, since the remote
Api classes became unavailable. We can replace them with spy objects, for which we import
Mockito into our test scope.
The separation between core and service also now demands a trade-off: The mapping logic inside of our
CheckoutPolicy services can’t be a part of the domain logic any more – it is specific to mapping to a JSON service, and thus belongs in the adapter.
One could argue that the information that, e.g., an order is created from a shopping cart’s items should be part of the domain. But at the same time, calling an external service is a technical detail, which certainly should not be.
We could move the Order class into
domain-core-shoppingcart, map it there, and send the translated item to the service, instead of the cart items. But that would require introducing a new
OrderMappingService, which to me feels quite arbitrary, since it is not motivated by the business itself, but by external forces.
This example illustrates a situation, which often arises in Domain Driven Design projects: There is a problem, that could be solved in several ways, all of which seem equally right and wrong, at least at first glance, and none are entirely convincing. We’ll have to decide for one, and maybe we will change our minds later — luckily, our Hexagonal Architecture allows us to try out different options without too much damage!
Once the core libraries are separated, we have created fully separate, true “share-nothing” microservices.
Interestingly enough, the topology looks exactly the same as in 2), but the logical diagram has changed:
Before we get to the more challenging next variations, let’s take a look back at 2) and 3): What if we had decided to do things in reverse order: split the core first, and then divide the services? Of course, that is a perfectly valid migration path! The implementation would have been quite similar, except for one crucial difference: Step 2 produced a new, more scalable deployment model. In a real life scenario, we might just as well have chosen to go the other way, and conservatively focused on the core, first. We chose the option which addressed the more pressing concern for our business – and that’s always a good rule of thumb.
To be continued in Part 3.
Special Thanks to Alexander Rose for helping to sort out both the language and content of this blog post. Again.