Hexagonal Architecture and Spring Boot, a powerful assembly option!
I have been studying DDD for a few month now, and just recently saw an opportunity to apply DDD to the new project we have started working on. I have learned a lot during my work on the project. Just recently, I found a some-what easy solution for our multi-client product we used to have. As any other software product, ours also has a number of features and modules. A client may buy a subset of these or maybe all of them. But how should we deliver this? A very simple approach could be, we would deliver a full functionality “binary” and then use configuration files to only enable a subset of those features. Yep that works, but is there a better way? A solution where I won’t give Full Functionality binary and only the subset which is used by the client. First let’s take a brief look at Hexagonal Architecture.
The key concept that we need to borrow from DDD here is that we have “Domain” modules and “Implementation Details” modules. Anything other than your Domain is considered an “implementation detail” and you should try to decouple your Domain business logic from them. For example, the Database your application is persisting data to, is an implementation detail. Your business logic does not really “depend” on it. If someone asked you to persist to a file, or use a web-service to do CRUD operations for your data, your business logic should almost never change.
Now comes the next part. Hexagonal Architecture or better called “Ports and Adapters pattern” is mainly giving us directions on how to address these. For an “Implementation Details” you would define an “API Port” which is technically an interface and an “Implementation Adapter” which is technically the concrete class implementing that interface. Let’s see over a diagram what we have here:
As you see we have our Domain and Services as the core of our Domain project. Around that we have defined API Ports for whatever “Implementation Details” we need to talk to. For example, our application is using JMS as the communication protocol to other modules. So we have defined an API Port for that. And the implementation is JMS Adapter. For this port we only have one implementation which is JMS, but that doesn’t mean we couldn’t decouple it yet! What if we want to switch to Kafka in the future? Then we will just have to provide a Kafka Adapter for our “Inter-Module API Port”.
The next Port we have used here is the Equipment API Port. This is used for our application to talk to an Equipment. Now, of course we don’t have access to the real Equipments on our automated test environment, but unfortunately even our Equipment Simulator is not test-friendly. Now that’s a real problem that needs to be solved, but is there a “temporary solution” to solve that? What if I decided temporarily not to test my “Vendor Adapters” on the module tests because they will be tested in our End-To-End Integration Tests? Ports and Adapters to the rescue! I have defined a Mock Adapter for my Equipment API Port where I can control its behavior, and will use that Adapter in my module test environment.
The last Port I am showing here is something that might not seem like a Port at first! We have a number of business flows in our code. In the old project we used an In-House solution for that. I have been trying to present Activti as a replacement, but that decision will take a very long time to be approved and we still need something to use for now! That’s why I have defined this Port for our Business Process Manager tool to decouple our Domain from the tool that will be used to run our Business Processes. There is one Adapter at the moment which is the In-House Adapter, but in the future, that could change into a BPMN solution.
So far, we have only talked about Ports and Adapters! So what about the assembly? Having all these Ports and Adapters is bringing up the assembly problem that we talked in the beginning. I have multiple Adapters for the same Port and only one of them can be used at a time. How do we assemble our product now? We have multiple assemblies and we’d really prefer not to have a jar file in an assembly if it is not used. Basically, this means we have a couple of modules that exist in every assembly (e.x. Domain module or JMS Adapter module) and a couple of modules that exist in some of the assemblies only (e.x. the Mock Adapter). Let’s call the first modules “Mandatory Modules” and the latter “Optional Modules”. For example, our Client X assembly would look something like this:
Let’s create an assembly maven project. This project has the main class to run our application. The main class is a SpringBootApplication config class also.
Now Mandatory Modules are easy. On your maven pom file you declare a direct dependency on those and on the main Spring configuration class you will just Import those module’s configuration classes. Now optional modules are tricky, because we want them for only some of the assemblies and we can not have compile dependency on them.
For the dependency part, you could use a maven profile that represents one of your clients, let’s call it “Client X”. On this profile you can add dependencies to your assembly project which is specific for that client (the Optional modules that are needed for this Client). But how about the configuration? Spring Boot AutoConfigure can help us cover that. This means, on the optional modules, we are saying, if a Spring Application has enabled AutoConfiguration and has my jar file on their classpath, then load my configuration automatically for them. So, as long as our assembly includes an Optional Module, it will be auto-loaded by Spring.
Now, if you use Spring’s fat jar plugin, you should define a new execution for that plugin to create another jar with a different classifier:
Now, when building the project, you can specify the client-specific maven profile when building your assembly and you will get a fat jar specific to that client.