I recently worked on a new project and decided to use Vertical Slices approach - this is how it turned out.
Background
The basic idea of Vertical Slices is to:
Keep together what is likely to change together.
A Vertical Slice Architecture organizes functionality by features, prioritizing feature autonomy. It promotes a high cohesion within a slice and loose coupling between slices.
My approach differs slightly from the commonly adopted one. My slice has a public contract (DTOs) and includes nothing related to presentation (or more generally I/O concerns). Instead, the presentation layer converts the slice contract to whatever is needed for the presentation. This is partly because slices were hosted in a web and a console application. It also enabled me to combine data from multiple slices for presentation. Another difference is that I didn’t use Mediator pattern - just plain old interfaces and DTOs.
Implementation
To keep it simple, I put all the slices in a single assembly. I started the implementation with slices containing exactly one feature. I wasn’t sure how the features would relate and didn’t want to overthink it.
The next step was to introduce data storage. I was using a relational database and an ORM, and the first red flag was that my slices shared the database model. Effectively, the database was introducing implicit coupling.
Further down the road, it got even worse. There were a lot of dependencies between slices.
At this point you may say “Hey, this is a total mess!” and it will be the truth.
On the positive side, however, I have accomplished two important goals:
- had a working proof of concept of my core features and have learned a lot about the domain and the business problems I was solving.
- had technical experience with concrete implementations and knew what worked well and what didn’t.
So it was time to stop adding new features and address the problems armed with the gained knowledge.
First I looked at the coupling between slices. The slices that were “chatting” too much appeared to be different features serving the same business capability. Also, they were likely to change together, so it was natural to merge them into one slice so they could work together without additional overhead.
There is nothing wrong with one slice using functionality another slice exposes. Beware of circular dependencies or chained dependencies - it hints that slice boundaries are not in the right place. Another pitfall is a shared service. If many slices depend on the same service, it’s a red flag, and dependencies need review and analysis.
The second problem, implicit coupling by the data model, was more subtle. To analyze the dependencies I wrote down all the slices on one side and all database entities on the other and connected them based on the usage. It was possible to introduce a separate ORM model per slice but it would solve only half of the problem. The database tables would still be shared between models which will be a pain in the future for sure.
One of the slices was responsible for ingesting external data and the relational model was a good fit for the task. The rest of the slices, however, were using only a de-normalized subset of the data. Their data access often consisted of the same four to six SQL joins over data that rarely changes once ingested.
So there was a tradeoff decision - physically de-normalize the data into separate tables at the cost of data duplication. I decided to go this way and it paid off - the slices were now “owning” their database models, they were using only the data they needed, and most important the implicit coupling was gone. To enforce the separation on a database level, each module owned a schema. It was forbidden for a module to use database objects from another schema.
The last part was propagating the changes from the relational model to its de-normalized versions. There are multiple choices for solving this, such as using database triggers, or the slice changing the data to call other slices.
My choice was to introduce in-memory events with the publish-subscribe pattern. This somewhat resembles CQRS but it’s not, since each slice decides how to deal with events and has its read-write logic.
This is what my final solution looked like:
I have gradually transitioned from Vertical Slices to Modules.
Aftermath
It appeared that this modular organization of the code had some advantages:
- Each module deals only with the data it needs, and since data is stored in separate tables, special indexing can be applied per slice needs.
- Utilizing de-normalized data, I can move it to a more suitable storage solution, like a document database for example. The decision can be made per module.
- Introducing events opens up additional possibilities, for example, Event Sourcing can be used if feasible. Again the decision can be made per module.
- Events also allow modeling more complex processing without additional coupling, using patterns like Choreography or Orchestration.
- Modules can be scaled independently by extracting them into microservices. There is a clear path to it - events to be moved to an external message broker, data to be moved to a separate storage, module public interface to become an API callable over the network.
Additionally, using Vertical Slices, and consequently - Modules, made me frequently analyze, revise, and refine my domain model and its interactions. Paying attention to the domain cohesion within a slice/module can result in merging or splitting them. It is all about finding the correct boundaries.
Conclusion
I find using Modules quite appealing. It encourages an iterative approach, gaining and using knowledge along the way, pairing it with frequent refactoring and adjusting the boundaries. It resulted in a natural grouping of functionality making the whole solution more understandable and aligned with the business problems.