Vertical Slice Architecture in ASP.NET Core: A Modern Approach to Building Maintainable Applications
In the ever-evolving landscape of software development, architects and developers continuously seek better ways to organize and structure their applications. Traditional layered architectures, while familiar and well-established, often introduce complexity and maintenance challenges as applications grow. Enter Vertical Slice Architecture (VSA), a revolutionary approach that's transforming how we build ASP.NET Core applications by organizing code around features rather than technical layers.
Overview
Vertical Slice Architecture represents a paradigm shift from the conventional horizontal layering approach. Instead of separating concerns across technical boundaries like presentation, business logic, and data access layers, VSA cuts through these layers vertically, creating self-contained feature slices that encompass everything needed to fulfill a specific business requirement.
Vertical Slice Architecture is an increasingly popular approach for building feature-centric ASP.NET Core applications. Rather than organizing code strictly by technical layers (like UI, business logic, data), vertical slicing groups everything needed for a single feature or use case together. In other words, each slice is a mini application: it contains its own models, logic, validation, database access, and API endpoints. This can dramatically improve maintainability and modularity. By treating every feature as an independent component, teams can add or change functionality without touching unrelated parts of the codebase.
Vertical slicing aligns well with ASP.NET Core patterns like Minimal APIs or MVC. In practice, you might create a Features folder in your solution, and under it one folder per feature (e.g. Users, Products). Each feature folder then contains request and response models, MediatR commands/queries, handlers, validators, and controllers or endpoint definitions. This contrasts with traditional layered structures.
What Is Vertical Slice Architecture?
In a vertical slice design, every feature or use case is developed in isolation. Each slice “covers all the layers of a traditional architecture” so you don’t need to jump across projects or folders to implement a feature. For example, consider a “Create Order” feature: one vertical slice might include the API endpoint, the command and handler, input/output models, and data access code – all in one place.
By design, vertical slices often follow the CQRS pattern (Command/Query Responsibility Segregation). In practice, you break incoming requests into commands (for writes) and queries (for reads). Each command or query is handled by its own MediatR (or similar) handler within the feature slice. This enforces a clean separation of responsibilities and often leads to code that is easy to test and reason about.
Vertical Slice Architecture flips the usual organization of a layered or clean architecture. Instead of a “layer cake” (where controllers, services, repositories, etc. are in separate projects or folders), each vertical slice is like a layered piece of the cake that stands on its own. The result is very high cohesion within a slice and low coupling between slices.
Understanding Vertical Slice Architecture
1. Core Principles
- Vertical Slice Architecture is built on the principle of organizing code by features rather than technical concerns. Each vertical slice represents a complete cross-section of the application stack, containing all the necessary components to implement a specific business capability – from the API endpoint to the database interaction.
- This approach treats each request as a distinct use case, naturally leading to the implementation of Command Query Responsibility Segregation (CQRS) patterns. Commands handle write operations (POST, PUT, DELETE), while queries manage read operations (GET), creating a clean separation of concerns within each slice.
2. Key Characteristics
- Feature-Centric Organization: Rather than spreading a single feature across multiple layers, VSA groups all related code for a specific feature in one cohesive unit. This includes controllers, business logic, data access, validation, and response models, everything needed to complete the feature from start to finish.
- Self-Contained Slices: Each vertical slice operates independently, containing its own request/response models, handlers, validators, and data access logic. This independence allows different slices to evolve separately without affecting others.
- Reduced Coupling: By organizing around features, VSA significantly reduces coupling between unrelated business capabilities while maintaining high cohesion within each feature.
Project Structure and Organization
1. Folder Structure Approach
The most effective way to implement Vertical Slice Architecture in ASP.NET Core is through the Features folder approach. This method organizes code by business capabilities, creating a clear and intuitive project structure.
Figure-1: Vertical Slice Architecture Project Structure in ASP.NET Core
2. Implementation Patterns
Deep vs. Shallow Organization: You can structure your features in two primary ways:
- Deep Organization: Each feature has its own folder with subfolders for different operations (Create, Read, Update, Delete)
- Shallow Organization: All operations for a feature are kept in the same folder level
- CQRS Integration: Since each slice typically represents either a command or query operation, VSA naturally embraces CQRS patterns. Commands handle state changes, while queries retrieve data, each with their own models and handlers.
3. Sample Project Structure
eCommerce.API/ ├── Features/ │ ├── Products/ │ │ ├── CreateProduct/ │ │ │ ├── CreateProductCommand.cs │ │ │ ├── CreateProductHandler.cs │ │ │ └── CreateProductValidator.cs │ │ ├── GetProduct/ │ │ │ ├── GetProductQuery.cs │ │ │ ├── GetProductHandler.cs │ │ │ └── GetProductResponse.cs │ │ └── UpdateProduct/ │ ├── Orders/ │ └── Common/ ├── Controllers/ └── Infrastructure/
Vertical Slice vs Traditional Layered Architecture
Traditional layered (or n-tier/clean) architectures organize code by technical concerns: for example, an API layer (controllers), an Application/Service layer, a Domain/Business layer, and a Data layer. This enforces separation of concerns but often leads to low coupling between layers and high coupling within layers. In practice, adding a new feature in a layered app typically means touching many layers: creating controllers, services, repositories, and data model changes separately. This can slow development and increase bugs when developers miss one of the layers.
By contrast, Vertical Slice Architecture treats each feature (or use case) as an end-to-end slice. You “couple along the slice” instead of “across a layer”. In other words, you might duplicate similar logic in two slices if it keeps features cleanly separated.
Here are some key differences:
- Organization: Layered architecture is organized by layers (e.g. Projects: API, Application, Domain, Infrastructure). Vertical Slice organizes by features (Projects or folders: each feature).
- Coupling: Layered architecture has loose coupling between layers but forces changes across layers for one feature. Vertical Slice has loose coupling between features (slices) and tight cohesion within each slice.
- Abstractions: Layered code relies on shared abstractions like repositories or services used by many features. Vertical slices minimize or eliminate those cross-feature abstractions.
- Scalability: A monolithic layered app can become hard to scale as features grow. With slices, you could even evolve each slice into a microservice later.
Of course, layered architectures are not inherently bad, but teams have found that adding new features in them can be tedious. Vertical slicing simplifies this: you change one slice instead of many layers. The trade-off is that slices may start with some duplicated code, but this is often a worthwhile price for clearer feature boundaries.
Figure-2: Comparison of Traditional Layered Architecture vs Vertical Slice Architecture
Benefits of Vertical Slice Architecture
1. Enhanced Maintainability
The primary advantage of VSA is its exceptional maintainability. When you need to modify a feature, all the related code resides in a single location. This eliminates the need to hunt through multiple layers and folders to understand how a feature works or to implement changes.
- Reduced Merge Conflicts: Since each feature is self-contained, different developers can work on separate features without frequently editing the same files. This significantly reduces merge conflicts and enables safer parallel development.
- Faster Onboarding: New team members can quickly understand and contribute to specific features without needing to comprehend the entire application's architecture. They can focus on one vertical slice at a time, reducing the learning curve.
2. Development Efficiency
- Independent Development: Teams can work on different features simultaneously without interfering with each other. Each slice can be developed, tested, and deployed independently, supporting agile development practices.
- Flexible Technology Choices: Different slices can use different technologies or approaches as needed. One slice might use Entity Framework Core, while another uses raw SQL or stored procedures, depending on the specific requirements.
- Append-Only Development: Adding new features typically involves creating new files rather than modifying existing ones, reducing the risk of introducing bugs into working functionality.
3. Reduced Cross-Feature Coupling:
Since each feature lives in its own slice, code unrelated to that feature simply doesn’t interact with it. If you want to work on one feature, you only deal with that slice’s code, not dozens of other files.
4. Better Feature Grouping and Discoverability:
All files for a feature are kept together, often in one folder. New developers or maintainers can easily find everything related to a feature without digging through layers. Similarly, the .NET example template emphasizes placing “all the relevant things close together” within the feature folder.
5. Independent Evolution of Features:
Each slice can even use different technologies under the hood. For example, one feature could use Entity Framework Core, while another might use Dapper or call external services, all without interfering with each other. This allows teams to optimize each slice as needed.
6. Natural CQRS Implementation:
Vertical slices lend themselves to CQRS. Commands (POST/PUT/DELETE) and Queries (GET) are separate paths, each handled in its feature slice. Because the slices drive CQRS, introducing a new feature simply means adding new code for that request – you don’t have to modify existing shared code.
7. Easier Testing and Refactoring:
With feature slices, you can test each feature in isolation, mocking only its dependencies. Also, because features are isolated, you can refactor a single slice without worrying about breaking others, as long as you follow good domain-driven design practices.
Overall, vertical slice architecture can make large applications more modular and easier to maintain. By focusing on features, you stop navigating through multiple layers for a single change. As a .NET solution template author puts it, the goal is to “stop thinking about horizontal layers and start thinking about vertical slices”.
Figure-3: Benefits and Drawbacks of Vertical Slice Architecture
Implementing Vertical Slices in ASP.NET Core
1. Solution Structure
In practice, an ASP.NET Core project using Vertical Slices often looks like this:
- Data/Infrastructure Folder: Contains database context (e.g. EF Core DbContext), data access, and any common infrastructure (logging, etc.). This is separate from features because it’s truly shared or cross-cutting.
- Domain/Entities Folder: Contains domain models or entity classes that are used by features (e.g. User, Order entities). These are shared across features if multiple slices refer to the same domain concepts.
- Features Folder: This is the heart of the vertical slice structure. Each subfolder here is a self-contained feature/slice. For example:
- Features/Users/GetUser
- Features/Users/CreateUser
- Features/Products/GetProduct
- Features/Orders/CreateOrder
Inside each feature folder, you typically have:
- Request and Response Models: e.g. GetUserQuery, GetUserResponse.
- MediatR Command/Query and Handler: e.g. GetUserQueryHandler implements the logic.
- Validator (optional): If using FluentValidation, something like GetUserQueryValidator.
- Endpoint Definition or Controller: If using Minimal APIs, a static method to map endpoints; if using MVC, a controller class or controller action for that feature.
- Other Helpers: Maybe an AutoMapper profile or any custom types just for that slice.
This matches what CodeMaze describes: “Each subfolder inside the Features folder is a self-contained use case”, with MediatR request/response classes and result classes.
By structuring this way, all code needed for a use case (end-to-end) is colocated. A .NET 9 template example emphasizes that “business logic is placed in a Feature folder” and “all the relevant things close together” for that feature. There is no separate Controller file to search for; the endpoint lives with the handler.
2. Using MediatR and CQRS
A common pattern is to use MediatR (a popular .NET library) for handling commands and queries. With MediatR, you define a request object (like CreateOrderCommand) and a handler (CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderDto>). You then register MediatR in Startup/Program so it auto-discovers handlers. In a vertical slice, each feature folder has its own request and handler.
For example, inside Features/Orders/CreateOrder, you might have:
... public class CreateOrderCommand : IRequest<OrderResult> { /* properties */ } public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, OrderResult> { public Task<OrderResult> Handle(CreateOrderCommand cmd, CancellationToken ct) { // business logic for creating order } } ...
Then in your API code (e.g. in a controller or minimal API route), you send this command:
... var result = await mediator.Send(new CreateOrderCommand { /*...*/ }); ...
This keeps the ASP.NET Core dependencies (controllers, HTTP) decoupled from the business logic. The vertical slice folder contains everything about “Create Order” – the HTTP request model, the handler logic, any validation, etc.
3. Setting Up Endpoints
In ASP.NET Core, you expose each slice via an endpoint. You can use MVC controllers or Minimal APIs. With MVC, you might have one controller per feature (or use attribute routing to group by feature). With Minimal APIs, you’ll typically map endpoints in Program.cs or extension methods.
For example, using Minimal APIs:
... var builder = WebApplication.CreateBuilder(args); // Register feature services builder.Services.AddScoped<IOrderService, OrderService>(); builder.Services.AddDbContext<OrderDbContext>(options => ...); var app = builder.Build(); // Map endpoints for Order slice app.MapPost("/orders", async (CreateOrderCommand cmd, IMediator mediator) => { var result = await mediator.Send(cmd); return Results.Created($"/orders/{result.Id}", result); }); ...
Notice how we inject services directly into the endpoint. In the snippet above, the CreateOrderCommand and IMediator are parameters to the lambda. ASP.NET Core will bind CreateOrderCommand from the request body and resolve IMediator from DI.
For more complex features, it’s often better to move endpoint mapping into extension methods, per feature, to keep Program.cs clean. For instance, define an extension:
... public static class OrderEndpoints { public static void MapOrderEndpoints(this WebApplication app) { app.MapPost("/orders", (CreateOrderCommand cmd, IMediator m) => m.Send(cmd)); // other order-related endpoints... } } ...
Then in Program.cs:
... app.MapOrderEndpoints(); ...
This pattern (seen in code examplescodeproject.comcodeproject.com) reinforces the idea that each feature “owns” its routes and services.
4. Dependency Injection per Slice
Each slice typically has its own services and dependencies. In a layered app, you might have one giant ConfigureServices. In a vertical slice app, you can either still use Program.cs or split it. The example above shows builder.Services.AddScoped<IOrderService, OrderService>() and AddDbContext<OrderDbContext>() specifically for the Order slice. You might group those registrations into an extension method like AddOrderServices() to keep things organized.
It’s important to maintain independence of services:
- Keep each service logic specific to its slice, avoid cross-slice.
- Use interfaces and DI to decouple implementations.
- Use common/shared services (like logging, or an IDbContextFactory) carefully, without allowing one slice to call another slice’s service.
- If slices need to communicate, use well-defined patterns (domain events, shared kernels, internal API calls) rather than directly invoking code across slices.
By isolating slices in DI, you ensure that adding a feature (slice) only affects that slice’s registrations and endpoints. The rest of the app remains unchanged.
Challenges and Considerations
Vertical Slice Architecture has many benefits, but be mindful of:
Potential Drawbacks
- Code Duplication: One of the primary concerns with VSA is the potential for code duplication across slices. However, this isn't necessarily a violation of the DRY principle if the duplicated code serves different business contexts.
- Inconsistent Implementation: Since each slice can be implemented differently, maintaining consistency across the application can be challenging. This requires strong code review processes and clear guidelines.
- Cross-Cutting Concerns: Implementing shared concerns like logging, authentication, and validation across multiple slices can be more complex than in traditional layered architectures.
- Learning Curve: Developers must get used to thinking feature-first instead of layer-first. Teams unfamiliar with this may initially find it strange not to have one service class per entity.
- Maintaining Domain Integrity: Since logic lives in handlers, it requires discipline to refactor common logic into domain objects.
- Cross-Slice Dependencies: Directly referencing one slice’s repository or classes from another would break isolation. Instead, use events or shared interfaces. To avoid this, use patterns like a Shared Kernel for common concepts or domain events for decoupling.
- Over-Engineering: In very small projects, vertical slice might add complexity. It shines in medium-to-large, feature-rich apps where clear boundaries pay off. For trivial CRUD apps, the overhead may not be worth it.
In summary, Vertical Slice Architecture in ASP.NET Core shifts the focus from “how do I organize my layers?” to “how do I implement this feature end-to-end?”. It often pairs naturally with clean code practices and test-driven development. When done right, new features are easy to add (just create a new slice) and existing code remains stable. As one pro comments, each slice becomes “the master of its own destiny”, you can change one without fear of unintended side effects.
Best Practices and Patterns
- Feature Folder Naming: Name feature folders by business terms or use cases (e.g. CreateOrder, GetUser, ProcessPayment). Consistency helps readability.
- Minimal Duplication: A small amount of duplication between slices is acceptable to keep boundaries clear. For example, two slices might each validate an email field in slightly different ways; that’s fine.
- Use CQRS Conventions: Stick to clear command/query separation. Name classes accordingly (e.g. GetProductQuery vs UpdateProductCommand).
- Handle Cross-Slice Communication Properly: If one feature needs data from another, do it via events or shared services, not by reaching into another slice’s internals. For instance, you might publish a UserRegistered event from the User slice and let other slices (via a shared event bus) react, keeping the slices decoupled.
- Testing: Write unit tests per slice by mocking external dependencies. Since each slice has its own handler and possibly its own service, you can isolate it easily in tests.
- Iterate from Simple to Complex: Start with simple implementation in the slice (e.g. Transaction Script). As the feature grows, refactor the logic (e.g. move complex rules to domain services or sub-classes) within the same slice.
- Do Not Ignore Domain Logic: While slices hold a lot of logic, beware of creating “god commands” that do everything. If a slice’s handler becomes unwieldy, extract and refactor into domain classes or services within that slice.
Libraries like MediatR or Wolverine are often used. Wolverine in particular “leans into” vertical slice patterns and CQRS (though that’s an advanced choice).
Best Practices for Mitigation
- Shared Components: Create a Common or Shared folder for truly reusable components that serve multiple slices. This includes cross-cutting concerns, shared domain models, and utility functions.
- Consistent Patterns: Establish and enforce consistent patterns across slices through code reviews, linting rules, and architectural guidelines.
- Strategic Abstraction: Don't abstract prematurely, but do create abstractions when they provide genuine value across multiple slices.
Testing Strategies
1. Unit Testing Approaches
- Handler Testing: Focus your unit tests on the handlers, which contain the core business logic. Mock external dependencies like databases and external services.
... [Test] public async Task CreateProductHandler_ShouldCreateProduct_WhenValidRequest() { // Arrange var mockContext = CreateMockDbContext(); var handler = new CreateProduct.Handler(mockContext); var command = new CreateProduct.Command("Test Product", 99.99m); // Act var result = await handler.Handle(command, CancellationToken.None); // Assert Assert.That(result.Name, Is.EqualTo("Test Product")); Assert.That(result.Price, Is.EqualTo(99.99m)); } ...
- Integration Testing: Test entire slices with real dependencies to ensure proper integration. Use in-memory databases or test containers for isolation.
2. Testing Philosophy
- Feature-Focused Testing: Align your tests with your vertical slices. Test each feature comprehensively, including happy paths, error conditions, and edge cases.
- Minimal Mocking: Since slices are self-contained, you can often test them with minimal mocking, using real implementations where possible.
When to Use Vertical Slice Architecture
Ideal Scenarios
- Feature-Rich Applications: VSA shines in applications with many distinct features that can be developed independently. E-commerce platforms, content management systems, and business applications are excellent candidates.
- Agile Development: Teams practicing agile methodologies benefit from VSA's feature-centric approach, as it aligns perfectly with user stories and sprint planning.
- Medium to Large Teams: VSA enables multiple developers to work on different features simultaneously without conflicts.
When to Avoid Vertical Slice Architecture
- Simple CRUD Applications: If your application primarily performs basic Create, Read, Update, Delete operations without complex business logic, traditional layered architectures might be simpler.
- Single-Developer Projects: The overhead of VSA might not be justified for small projects with a single developer.
Combining Vertical Slice Architecture with Other Architectures
1. Hybrid Approaches
- VSA + Clean Architecture: You can combine the organizational benefits of VSA with the dependency management principles of Clean Architecture. Use vertical slices for feature organization while maintaining clean dependency directions.
- Bounded Context Integration: VSA works excellently with Domain-Driven Design's bounded contexts. Each bounded context can contain multiple related vertical slices.
2. Evolutionary Architecture
- Gradual Adoption: You can migrate from traditional architectures to VSA incrementally. Start with new features using vertical slices while gradually refactoring existing code.
- Technology Migration: VSA's feature independence makes it easier to migrate technologies one slice at a time. You can update Entity Framework versions, change databases, or adopt new patterns without affecting the entire application.
Performance and Scalability Considerations
1. Performance Benefits
- Focused Optimization: Each slice can be optimized independently based on its specific requirements. High-traffic features can receive performance attention without affecting others.
- Reduced Dependencies: Fewer shared dependencies mean faster compilation times and reduced runtime overhead.
2. Scalability Patterns
- Microservices Evolution: Vertical slices can easily evolve into microservices when needed. Each slice already contains all the necessary components for independent deployment.
- Horizontal Scaling: Features can be scaled independently based on usage patterns. Popular features can be deployed to more instances without scaling unused functionality.
Security Considerations
1. Authentication and Authorization
- Cross-Cutting Security: Implement security concerns as middleware or filters that apply across all slices. Use ASP.NET Core's built-in security features for authentication and authorization.
- Feature-Specific Security: Some features may require specific security measures. VSA allows you to implement these without affecting other parts of the application.
2. Data Protection
- Isolated Data Access: Each slice can implement its own data protection strategies. Sensitive features can use additional encryption or access controls.
Deployment and DevOps
CI/CD Integration
- Feature-Based Pipelines: Set up CI/CD pipelines that can build and test individual features. This enables faster feedback and more targeted deployments.
- Independent Deployment: While typically deployed as a monolith, VSA's independence makes it easier to split into separate deployments when needed.
Monitoring and Observability
- Feature-Level Metrics: Implement monitoring that tracks performance and errors at the feature level. This provides better insights into which parts of your application need attention.
- Distributed Tracing: Use distributed tracing to track requests across different slices, especially when they interact with each other.
Conclusion
Vertical Slice Architecture offers a modern, feature-based way to structure ASP.NET Core applications. By organizing code by use case instead of by layer, it improves modularity, maintainability, and developer productivity. Each feature slice acts like a mini-application containing its models, logic, data access, and endpoints, which simplifies development. In practice, this means teams can build and evolve complex systems with less cross-component friction.
That said, vertical slicing is a paradigm shift from traditional layering. It requires discipline to manage dependencies and refactor when slices grow. Effective automated testing is crucial to ensure slices remain robust over time. But for projects with many features or frequent changes, the vertical slice approach often pays off. It aligns naturally with ASP.NET Core’s dependency injection and Minimal API patterns, and with libraries like MediatR for CQRS.
Ultimately, Vertical Slice Architecture turns the question around: instead of “which layer should this go in?”, you ask “which feature/use-case slice does this belong to?” When each feature is its own self-contained slice, teams can deliver new functionality faster and keep the codebase easier to understand.
Think in features, not layers. In ASP.NET Core, group everything a feature needs in one place, use MediatR or minimal API endpoints to implement it, and let your slices grow independently. This way, adding a feature means adding code, not modifying a dozen layers, a truly liberating approach.
Vertical Slice Architecture represents a significant evolution in how we structure ASP.NET Core applications. By organizing code around features rather than technical layers, VSA provides numerous benefits including improved maintainability, reduced coupling, independent development, and better alignment with business requirements.
While VSA isn't a silver bullet and comes with its own challenges, such as potential code duplication and the complexity of managing cross-cutting concerns, the benefits often outweigh the drawbacks for most modern applications. The key is understanding when and how to apply VSA effectively.
For teams building feature-rich applications with multiple developers, VSA offers a compelling alternative to traditional layered architectures. It supports agile development practices, enables independent feature development, and provides the flexibility to choose the right technology for each specific use case.
As you consider implementing Vertical Slice Architecture in your next ASP.NET Core project, remember that it's not mutually exclusive with other architectural patterns. You can combine VSA with Clean Architecture principles, Domain-Driven Design, and other patterns to create a robust, maintainable, and scalable application architecture.
The future of software architecture is moving toward more flexible, feature-centric approaches that align with how businesses actually work. Vertical Slice Architecture positions your applications to be more maintainable, scalable, and adaptable to changing requirements, exactly what modern software development demands.