Software Architecture. Vertical Slicing Architectures
Introduction

- Vertical slicing: A technique to group the code by use cases or business capabilities, instead of technical layers. It allows more freedom and independence for each slice, and reduces coupling and complexity. It can be combined with concentric architectures such as onion or hexagonal.
- Modular monolith: An alternative to microservices that consists of breaking a large monolithic codebase into smaller modules that are loosely coupled and have clear boundaries. It is easier to change and refactor than microservices, and avoids the challenges of distributed systems.
- Finding module boundaries: A challenging task that requires balancing local and global complexity, aligning with business domains and teams, and avoiding scope creep and domain model dilution. The author suggests some tactics such as grouping by business capabilities, not concepts, and using domain-driven design principles.
- Decoupling modules: A necessary step to ensure the modules are cohesive and independent. The author recommends some strategies such as cutting domain entities at the boundary, using dependency inversion, applying event-driven architecture, and using feature toggles.
- How to decouple domain entities: The author explains that the first step is to unlink the object links between the entities and keep the foreign keys in the database. The second step is to identify the owner of data based on who changes it. The third step is to create an internal API for each module and use data decoupling objects to communicate with other modules.
- How to deal with cyclic dependencies: The author suggests to question the reasons for having cyclic dependencies and to consider alternatives such as extracting common modules, using events, applying dependency inversion, creating a facade API, or merging the modules back.
- How to test a modular monolith: The author recommends to use integration tests as the default choice and to test the whole story of each use case. The author also shows a testing pyramid for microservices and compares it with a testing honeycomb for modular monoliths.
- How to release a modular monolith: The author acknowledges that there is some friction in releasing a modular monolith with multiple teams involved, but also shares an example of a team that managed to release it every week by optimizing their pipeline. The author advises to invest in automation and to explore refactoring opportunities.
Implementation considerations
-
How to decouple domain entities: The author explains that the first step is to replace the object references between the entities with primitive types that represent the foreign keys in the database. For example, instead of having a
Customerobject in anOrderobject, you would have acustomerIdfield of typeLong. The second step is to assign the responsibility of data based on who modifies it. For example, if theOrdermodule changes thestatusof an order, then theOrderentity should own thestatusfield. The third step is to define an interface for each module and use DTOs (Data Transfer Objects) to communicate with other modules. For example, if theCustomermodule needs to access some data from theOrdermodule, it should use theOrderServiceinterface and theOrderDTOclass. - How to deal with cyclic dependencies: The author suggests to reconsider the reasons for having cyclic dependencies and to explore alternatives such as extracting common modules, using events, applying dependency inversion, creating a facade API, or merging the modules back. For example, if the
Ordermodule depends on theCustomermodule and vice versa, you could extract aPaymentmodule that both modules depend on, or use an event bus to publish and subscribe to events between the modules, or invert the dependencies by using abstract interfaces, or create aCustomerOrderServicethat acts as a facade for both modules, or simply combine theOrderandCustomermodules into one.References
- Victor Rentea