Software architecting is about tradeoffs. Even when the theory is good the implementation details can break it. In this article I try to find the best from two architectural approaches: Package by component and Clean architecture (a variety of Ports and adapters).
Package by component, as proposed by Simon Brown in his Missing chapter, is an architectural approach organizing code based by bundling together everything related to a component. If done properly (only the component entry point is marked as public) enforces architectural rules, like not bypassing the busines logic in the infrastructure layer - problematic in the Ports and adaptes -, using only the standard Java compiler mechanism.
Enforced Separation of Concepts
In my experience it is very important to have such a strict mechanism, because otherwise the temptation to skip the rules is too strong, especially when working under time pressure (don't forget - the stress-driven architecture is the big ball of mud).
Package by component hides code under a package and so makes them inaccessible from another packages (components). There is no way to access the
OrderRepository from the
OrderController anymore and that's great.
The problem here is that there is no mechanism to prevent the access the
OrderRepositoryJdbc (infrastructure) from the
OrderServiceImpl (domain). The ease of using the implementation directly instead of the interface is still too high. It seems even in the Package by component approach is the separation of concepts enforced not enough.
Modules to Rescue
Fortunately we have more than only Java packages - we can configure separate modules or projects in a build tool (eg. Maven, Gradle, ...). We can use these artifacts as dependencies and hide so the "outside" from the "inside".
As you can see in the picture, there are no changes in the packages structure, we're still following the Package by component architecture, but this time the domain and infrastructure parts are both separated into modules. Because the domain artifact has no dependencies to the infrastructure artifact, accessing the
OrderRepositoryJdbc from the
OderServiceImpl would now cause a compilation error.
We don't have to stick with only two modules and can create a finer structure, for example modules like
We extend the "MyShop" application shown in the pictures above. We create a simple web application with products to add into a shopping cart and then to order.
As a glue we use the Spring framework (context) and for the web part the Spring Web/MVC all together with the Spring Boot.
Modules creates an onion structure:
- On the top there is the Spring Boot application layer - it represents a deployment scenario (alternatively there could be a WAR module for a web-container like WildFly etc.).
- Spring configurations, web UI and a database implementation create the second infrastructure layer - technical details.
- The domain layer sits always on the bottom - it provides the business API as well as implementation of the business logic.
Another benefit of using modules is that we can get rid of unnecessary dependencies in a declarative way. For example, if we want to add another implementation of the
OrderRepositoryInmem, we can do it in two ways: 1. adding a new class into the
db module, 2. creating two separate modules
db-inmem. Then we can exclude the dependencies in the
spring module, or just declare one of them respectively. With the second approach the implementation can be easily changed just by adding/removing a dependency, on the other hand the maintenance of an additional module is needed and in some cases this could lead to an explosion of modules.
The source code could be found on GitHub.
Note on the implementation: The purpose of the code is to show the Package by component with modules approach, nevertheless it is not a perfect Domain-driven design example. Following the DDD principles seriously, we should see a package
cz.net21.ttulka.myshop.cart, classes like
OrderItem, proper factories for domain objects and, of course, tests!