I couldn’t find any good project structure expressing the hexagonal architecture constraints for a Spring Boot microservice. So I decided to create this sample project with my insights.
That's beautiful. The ideal hexagonal architecture requires exactly six components. Your microservice project structure will look like this:
- thesystem (parent pom.xml)
- api (input adapter)
- app-interfaces (input port)
- app-services (application services)
- domain (domain)
- clients (output adapter)
- starter (launch & config)
That's perfect, right?
Not to me!
The ideal project structure has some drawbacks to me:
- I think that six maven modules are too much in the package explorer.
- I think declaring interfaces in
application interfaces
module and after implementing them in theapplication services
module is boring. - I like to pass
domain objects
to DTO's constructors, but I can't do that. I must createdomain to dto
mapper classes in theapplication services
module.
Let's try to reduce it to four maven modules.
- thesystem (parent pom.xml)
- api (input adapter)
- app (input port + application services)
- domain (domain)
- infra (output adapter + launch & config)
I have merged (input port + application services) in the app
module. But wait:
What was the initial purpose of packing input ports
in a separated module having only interfaces and DTOs?
Short answer: To stop transitive dependency from the api module
to the domain
.
I saw many projects with application services interfaces and their implementations in the same module. There is no sense in doing that. In this situation, application services
should have no interfaces, only concrete classes.
Unfortunately, after merging, the transitive dependency came back. Now my domain objects
can leak to the api module
(outside the hexagon).
Fortunately, there is hope.
Let's see alternative techniques to solve that new problem.
<artifactId>api</artifactId>
<dependencies>
<dependency>
<groupId>com.thesystem</groupId>
<artifactId>app</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.thesystem</groupId>
<artifactId>domain</artifactId>
</exclusion>
</exclusions>
</dependency>
...
</dependencies>
- Maven exclusions will protect the
api module
from returning and receiving domain objects as parameters (compile-time dependency). That's good! - On the other hand, this will not prevent DTOs from having domain objects inside them (runtime dependency). Fortunately, I can ensure this won't happen by doing some runtime checks in build time using the incredible archunit tool. Please, see the test class
com.thesystem.test.ArchitectureTest
. This tool allows me to usedomain objects
in the DTO's constructor but disallows me to have them as property or method returns. That's beautiful. - Keep in mind if you want to stop more transitive dependencies like
spring-tx
, you will need to add more maven exclusions.
- The
requires
java modules directive, when used alone, stops all transitive dependencies by default. That's very good! - On the other hand, java modules need management of the
module-info.java
files hierarchy that produces some noise. That's also, in some sense, a duplication of maven's work. Finally, you will be annoyed in some situations where everything works on the IDE but not in themvn
command. - You will need those runtime checks with the archunit tool as well.
The module infra is the sum of (output adapter + launch & config). There aren't side effects here. This merge just makes sense to me.