The way we build software keeps evolving. While Clean Architecture has been a favorite for .NET developers, Vertical Slice Architecture is gaining attention with its feature-focused approach. So, let’s take a quick dive into Vertical Slice, compare it with Clean Architecture, and see if it might be the right fit for your next project.
But, could Vertical Slice be the answer? Maybe. Traditional architectures can be rigid and tough to scale, sometimes making it hard to align code with business logic. Vertical Slice organizes everything by features, giving you more flexibility. But let’s be real — we won’t know if it’s perfect for every situation. It’s all about trying it out and seeing how it fits your needs. Ready to explore? Let’s jump in!
Vertical Slice Architecture, where each feature (e.g., “Create Books” and “Create Users”) moves through distinct layers: UI, Domain, Repository, and Database. Each slice is self-contained, handling its specific flow across all layers, ensuring modularity and feature isolation.
One of the ideal ways to understand Vertical Slice is by comparing it to a traditional layered architecture like Clean Architecture. Let’s see how they stack up!
Clean Architecture (left side):
- Layered like a cake: Clean Architecture breaks the app into layers, much like a cake. You’ve got your
Api.csproj
for the controllers (UI), and Application.csproj
for the business logic. Everything is neatly separated into its own layer.
- Commands and Features: In Clean Architecture, your commands like
CreateBookHandler.cs
and CreateUserHandler.cs
are tucked away in their own folder under Application. But here’s the catch—they’re split across multiple layers, so when you want to create a feature, you’ve got to bake a little in each layer!
Vertical Slice Architecture (right side):
- One feature, one slice: Now imagine that instead of splitting things up like a layer cake, you grab the whole feature and wrap it up into one neat package! That’s Vertical Slice. Every feature — like “Books” or “Users” — has its own little bundle with everything it needs inside.
- All-In-One: Your “Books” feature has its controller, data model, commands, and even the logic (
Book.cs
, CreateBookCommand.cs
, BooksController.cs
) all together. No need to jump between layers—it’s all right there in one slice. It’s like having all the ingredients for your favorite dish in one bowl!
So, what’s the big difference?
- Clean Architecture is great when you want that neat, layered separation of responsibilities. But it can feel a bit slow when you need to hop across layers to get a single feature done.
- Vertical Slice, on the other hand, is like the express lane! It’s feature-first, meaning everything you need for a feature is bundled up in one place. Easy to find, easy to change.
The challenge of isolation 😡: Feature isolation gone wrong
Alright, so we’re sold on the idea of Vertical Slice — it’s fast, it’s modular, and it keeps everything in nice little bundles. But here’s the catch: when you isolate features so completely, it can be a double-edged sword.
Imagine you’ve got your neat slices: one for Books, another for Users, and more for other features. Everything’s working fine until one day you need to access User data inside the Books slice — maybe to check if a user can borrow a book. Uh oh! Because these slices are independent, you can’t directly access the UserRepository
from the Books slice. This is where things can get tricky.
The image depicts a Vertical Slice Architecture where the “Books” feature’s ReserveBookQuery.cs
is directly accessing UserRepository.GetUsers()
from the "Users" feature. This breaks the principle of feature isolation, as each feature in Vertical Slice Architecture should be self-contained and independent. Directly sharing repositories across features creates tight coupling, making the system harder to maintain. Instead, shared logic like fetching users should be handled via a Shared Service or Shared Kernel to maintain feature isolation and modularity.
Solutions: Keeping the slices clean but connected
We want to keep our slices independent, but sometimes slices need to talk to each other. Here’s how you can handle this without turning your codebase into a tangled mess:
Shared Kernel to the rescue
The Shared Kernel is the cool middleman that lets slices share logic, data, and events without stepping on each other’s toes. Here’s how:
1. Shared services
Slices can call shared services like a UserService
to get common data or logic without creating dependencies between features.
Both slices (“Books” and “Users”) access shared logic or data through a UserService or similar service, located in a Shared Kernel that provides common functionality.
Example:
The “Books” slice uses UserService to grab user info when someone borrows a book.
• • •
2. Event-Driven communication
Slices publish and subscribe to events like BookBorrowedEvent
to trigger actions across the application while staying loosely coupled.
The image shows the Books feature triggering a BookBorrowedEvent.cs
in the Shared Kernel after running BorrowBookCommand.cs
. The Users feature listens to this event and updates user history in UpdateUserHistory.cs
. The event acts like a messenger, letting the two features talk without being directly connected, keeping everything modular and clean!
3. Exposed internal API endpoints
Slices expose internal API endpoints that other slices call to exchange data, like mini-microservices within the app.
Example:
The “Books” slice sends an HTTP request to the “Users” slice to fetch user details via /api/users/{id}
.
• • •
Now that we’ve explored how can slices communicate it’s important to remember that not all patterns fit every scenario. Each project, feature, and use case is unique, so be mindful and evaluate carefully when determining how your slices should communicate.
- For tightly coupled, reusable logic: Shared services in the Shared Kernel can be a great fit.
- For loose coupling and reacting to changes: Event-driven communication keeps things flexible.
- For structured inter-slice data exchange: Internal API endpoints are your mini-microservice solution.
Choosing the wrong communication method can create a tangled mess, so ask yourself: How independent do my slices need to be? Does my use case demand immediate responses or can I react to events asynchronously? Let the needs of your system guide your decision.
🚀 A developer’s transition: Clean Architecture to Vertical Slice
Shifting from Clean Architecture to Vertical Slice isn’t just a shift in code structure — it’s a shift in how you think about feature development and team collaboration. Here’s what to expect:
1. Faster feature development
- Less jumping around: With everything you need in one slice (commands, queries, handlers), adding new features is streamlined. No more navigating across layers or multiple projects.
- Immediate focus: Developers focus solely on the feature, reducing context switching and allowing for faster iterations.
• • •
2. Fewer merge conflicts
- Isolated feature slices: Since slices are self-contained, fewer developers touch the same files, reducing the chances of merge conflicts in large teams.
- Independent workflows: Teams can work on separate slices without stepping on each other’s toes, boosting overall productivity.
• • •
3. Boost in team productivity
- Feature-first mindset: Developers can deliver features end-to-end without dependencies slowing them down. No waiting for another team to update a service or repository.
- Parallel development: Teams can tackle multiple features simultaneously, thanks to slice independence, speeding up delivery cycles.
• • •
4. Clearer ownership
- Self-contained slices: Teams own their features entirely, from the presentation to the database logic. This makes responsibility clearer and avoids blame-shifting when bugs arise.
- Easier onboarding: New team members can focus on a single feature slice to understand the system, rather than navigating multiple layers.
• • •
5. Risk of ball of mud
- Temptation to overstuff slices: Be mindful of adding too much logic in one slice or directly calling methods from another slice. This could lead to a tightly coupled mess, undoing the benefits of modularity.
- Solution: Use Shared Kernels, Event-Driven Communication, or API Endpoints to handle cross-slice logic responsibly.
• • •
6. Code duplication vs. reuse
- Controlled duplication: You might face scenarios where the same logic needs to appear in multiple slices. While some duplication is acceptable, avoid over-reliance on shared services to preserve slice independence.
- Solution: Reuse common logic via Shared Services when absolutely necessary but respect the boundaries between features.
• • •
7. Communication complexity
- No free-for-all method calling: Unlike Clean Architecture, where services and repositories can be shared across layers, Vertical Slice requires a more thoughtful approach to communication between slices.
- Solution: Carefully evaluate when to use Events, Shared Services, or APIs for inter-slice communication to avoid tightly coupling features.
• • •
Final thoughts
Both Vertical Slice and Clean Architecture come with their own unique strengths and trade-offs. Vertical Slice really shines when it comes to speed in feature development. Since each feature is self-contained, there’s no need to navigate through multiple layers — everything you need is in one place. This simplicity allows developers to jump in and out quickly, reducing friction and enabling rapid iterations. However, when slices need to communicate with each other, this modularity requires careful management to avoid introducing complexity. The advantage is that it helps prevent the big ball of mud by keeping features isolated and manageable.
In contrast, Clean Architecture offers flexibility in terms of module reuse and separation of concerns. While it’s tempting to easily call methods across layers, doing so without discipline can lead to technical debt or tightly coupled systems, which can make maintenance harder in the long run. However, when combined with Domain-Driven Design (DDD) principles, Clean Architecture can effectively enforce clear boundaries between layers, keeping the core domain decoupled from external concerns and ensuring a scalable, maintainable structure.
A key point for Clean Architecture is its potential for layer reuse. For instance, persistence layers and domain layers can often be reused across platforms, such as web and mobile apps, by utilizing class libraries. This is more difficult to achieve in Vertical Slice, where each slice is tightly bound to a specific feature.
Choosing what’s best
There’s no one perfect condition where Vertical Slice or Clean Architecture is definitively the best. It all depends on your project’s needs, your team’s workflow, and your long-term goals. However, with the right set of use cases, team structure, and development priorities, Vertical Slice can offer an incredibly efficient way to build fast, isolated, and modular systems. Similarly, Clean Architecture can excel when reusability and separation of concerns are at the forefront of your project’s priorities.