Nearly ten years ago, after several negative experiences with what became known as “Clean Architecture,” I started on a new project in which I threw out most of the commonly accepted “best practices” around building SOLID architecture. I set out to rethink how our teams built and organized software. With this new approach, I vowed to let code smells—both large and small— drive design decisions, and developer heuristics drive the feedback loop.
The result of that exercise was the adoption of Vertical Slice Architecture. Today, we’ve delivered dozens of projects, large and small, using Vertical Slice Architecture, which has fulfilled all of the promises we saw broken with Clean-style architecture.
So then, should you use Vertical Slice Architecture for your project instead of a Clean or layered architecture? First, let’s look at how each of them functions:
Clean Architecture in a nutshell
Clean Architecture is a distillation of many different architectural types: Hexagonal, Onion, and others. The basic idea is to separate your software into individual layers. There is a taxonomy to these layers, and the rule is that lower layers cannot depend on code from a higher layer:
The lowest layer has no dependencies and the greatest level of isolation (above, it’s represented in circles to make the dependency directions explicit). Each subsequent layer depends on the layer below it, and often has additional external dependencies.
The idea behind this kind of architecture is to isolate logic so that it does not devolve into a big ball of mud. By enforcing strict rules about what kind of code belongs where, we try to fight software rot. When a developer needs to modify the system, they should know where to go to modify that code based on the layer that needs to be modified.
In order to enforce this layering, each layer is split into its own “module” for whatever modular approach the software runtime defines. For .NET-based applications, each layer would be a separate assembly and project, with project references enforcing the correct direction of dependencies. To deal with a lower layer needing a “dependency” only available in a higher layer, you create an abstract class or interface in the lower layer with an implementation in the higher layer.
Typical dependency diagram for Clean Architecture
The end application uses the DI framework to wire up abstractions to concrete implementations, such that at runtime, the lower layers use the correct implementation. Organization inside each layer can vary, typically by use case/controller/page in the web application, and lower layers by type or kind (Services, Repositories, Specifications, etc.)
Now that we’ve taken our tour of Clean Architecture, let’s look at how Vertical Slice Architecture differs.
Vertical Slice Architecture dissected
In this architectural style, we have two main tenets:
- Things that change together, belong together
- Maximize cohesion along axes of change and minimize coupling between them
These “axes of change” are the set of changes to a codebase needed to add some business functionality. I called these “vertical slices” because they cut across the typical layered architecture:
Instead of dividing code by “type” or kind, we organize it by the change we need to make. When we need to add business functionality to an application, these changes are “full stack,” meaning that they can span everything from the user interface, downwards. When we make changes to the application, we can minimize side effects by removing shared code or abstractions between different slices.
The next step is to encapsulate each slice so that our application doesn’t need to know what goes on inside the slice—it merely passes an input through and receives an output. We do this because, as we group our application code into slices, it needs to go “somewhere.” To rally around a uniform interface, I created MediatR, which represents the initial entry and exit points to these vertical slices. An implementation of the mediator pattern, it provides a simple interface to encapsulate the work done within a slice:
Inside the handler implementation, the developer is free to do whatever is needed to take the request and build the response. And because each slice encapsulates the work done from other slices, we can choose whatever implementation strategy makes the most sense for that particular slice. The YAGNI (You Ain’t Gonna Need It) principle factors heavily here: We don’t add abstractions or patterns until the code inside a single slice exhibits code smells that guide our refactoring.
The benefits of Vertical Slices vs. Clean Layers
When I looked back at our projects that used a “Clean”-style architecture, I found that the outcomes rarely delivered on the promises. The major problem we found with strict, layered architectures was abstractions: Lower layers use abstractions to interact with concrete dependencies.
Over and over again, we found that these assumptions were naive—our systems can’t pretend these dependencies don’t exist or ignore their fundamental properties, behaviors, and side effects. When we tried to build our core models and logic completely ignorant of external dependencies, we found ourselves bending over backwards to abstract them, or spending inordinate amounts of time trying to build abstractions that weren’t leaky, with very little value.
The first major difference between Vertical Slices and layered architectures is the lack of abstractions in our slices. Because each slice is encapsulated, it’s straightforward to change the implementation of a handler without affecting any other slice. Typically, an abstraction is introduced to shield the application from implementation details that would make it difficult to swap an implementation for another.
In practice, abstractions are quite difficult to swap implementations – especially in an incremental fashion. For example, if we abstract our database access or ORM behind a repository, it’s not feasible to swap out an implementation on a case-by-case basis, since the concretion is configured at the application layer.
With vertical slices, I can have one slice use an ORM, another use a micro ORM, and another use external APIs. These concrete dependencies may be used amongst many vertical slices, but the decision to move to another strategy doesn’t affect any other slice.
Refactoring to better design
One of the main assumptions of layered architectures is that our code is “cleaner” because it neatly separates concerns into layers. In practice, layers are a naive approach to separating code. With vertical slices, we start with the simplest solution that could possibly work. Once we prove our code works, we use standard code smells and refactoring techniques within that slice to clean it up.
With layers, our refactoring directions are limited because of a strict adherence to directional dependencies. We may want to combine services, split them up, or optimize some code somewhere. But since our code is spread across multiple modules/assemblies, it’s far more difficult to refactor/defactor.
Testing can become brittle as well, with mocks often used to substitute for abstractions in unit tests. If we want to change our implementation, we must change our mocks, further discouraging refactoring.
Keeps things together
One of the main frustrations with our teams using layered architecture was the difficulty in making a change in the application and understanding side effects. If we were to change a method, we would need to be very careful in understanding what other code was using this method. Placing code far away from where it is primarily used makes it much easier for other code to use it in the name of “reuse.” However, reuse introduces coupling.
Instead, a vertical slice architecture factors code into separate concerns, but without placing this code in disparate locations:
We can much more easily reason about the scope of an individual slice of functionality in our application when all the code relevant to that functionality lives in one spot.
Additions, not modifications
Perhaps the biggest quality of life improvements for our development team in moving to vertical slices was that new features resulted almost entirely in files added to an application, instead of methods or classes modified. This resulted in a great sense of confidence that the code the team was adding was not breaking any other slice of functionality.
In a layered architecture, on the other hand, services and implementations are shared between multiple entry points into the application. This means that the developer has to carefully analyze usages to ensure that changes won’t break anything:
The more tangled the usages, the higher probability something breaks.
So, why Vertical Slice?
While layered architectures and vertical slice architecture can safely co-exist in the same application, a vertical slice architecture ensures that any abstractions, encapsulations, or just plain refactorings are introduced when the need arises, and not before. By following a simpler approach from the start, we ensure that our code is only as complex as we need it.