Vertical Slice Architecture Is Easier Than You Think
Traditional layered architecture has served us well for decades, but as applications grow in complexity, the rigid horizontal layers can become a bottleneck. Enter Vertical Slice Architecture (VSA) – a pattern that organizes code around features rather than technical concerns.
What is Vertical Slice Architecture?
Vertical Slice Architecture organizes your application code by slicing vertically through all layers for each feature, rather than horizontally by technical layers. Each slice contains everything needed for a specific feature: controllers, business logic, data access, and models.
Why Choose Vertical Slice Architecture?
1. Feature Cohesion
All code related to a feature lives together, making it easier to understand and modify.
2. Reduced Coupling
Features are isolated from each other, reducing unintended dependencies.
3. Team Productivity
Teams can work on different features simultaneously without stepping on each other's toes.
4. Easier Testing
Each slice can be tested independently with focused, feature-specific tests.
Implementing VSA in .NET
Let's look at how to structure a .NET application using Vertical Slice Architecture:
src/
├── Features/
│ ├── Users/
│ │ ├── CreateUser/
│ │ │ ├── CreateUserCommand.cs
│ │ │ ├── CreateUserHandler.cs
│ │ │ ├── CreateUserValidator.cs
│ │ │ └── CreateUserController.cs
│ │ ├── GetUser/
│ │ │ ├── GetUserQuery.cs
│ │ │ ├── GetUserHandler.cs
│ │ │ └── GetUserController.cs
│ │ └── UpdateUser/
│ ├── Orders/
│ │ ├── CreateOrder/
│ │ ├── ProcessOrder/
│ │ └── CancelOrder/
│ └── Products/
├── Shared/
│ ├── Database/
│ ├── Exceptions/
│ └── Extensions/
└── Program.cs
Example Implementation
Here's how a feature slice might look:
CreateUserCommand.cs
csharp1public record CreateUserCommand( 2 string Email, 3 string FirstName, 4 string LastName) : IRequest<CreateUserResponse>; 5 6public record CreateUserResponse( 7 Guid Id, 8 string Email, 9 string FullName);
CreateUserHandler.cs
csharp1public class CreateUserHandler : IRequestHandler<CreateUserCommand, CreateUserResponse> 2{ 3 private readonly ApplicationDbContext _context; 4 private readonly IValidator<CreateUserCommand> _validator; 5 6 public CreateUserHandler( 7 ApplicationDbContext context, 8 IValidator<CreateUserCommand> validator) 9 { 10 _context = context; 11 _validator = validator; 12 } 13 14 public async Task<CreateUserResponse> Handle( 15 CreateUserCommand request, 16 CancellationToken cancellationToken) 17 { 18 var validationResult = await _validator.ValidateAsync(request, cancellationToken); 19 if (!validationResult.IsValid) 20 { 21 throw new ValidationException(validationResult.Errors); 22 } 23 24 var user = new User 25 { 26 Id = Guid.NewGuid(), 27 Email = request.Email, 28 FirstName = request.FirstName, 29 LastName = request.LastName, 30 CreatedAt = DateTime.UtcNow 31 }; 32 33 _context.Users.Add(user); 34 await _context.SaveChangesAsync(cancellationToken); 35 36 return new CreateUserResponse( 37 user.Id, 38 user.Email, 39 $"{user.FirstName} {user.LastName}"); 40 } 41}
CreateUserController.cs
csharp1[ApiController] 2[Route("api/users")] 3public class CreateUserController : ControllerBase 4{ 5 private readonly IMediator _mediator; 6 7 public CreateUserController(IMediator mediator) 8 { 9 _mediator = mediator; 10 } 11 12 [HttpPost] 13 public async Task<ActionResult<CreateUserResponse>> CreateUser( 14 [FromBody] CreateUserCommand command, 15 CancellationToken cancellationToken) 16 { 17 var response = await _mediator.Send(command, cancellationToken); 18 return CreatedAtAction( 19 nameof(GetUserController.GetUser), 20 new { id = response.Id }, 21 response); 22 } 23}
Using MediatR for Coordination
MediatR is an excellent companion to VSA, providing a clean way to handle requests:
csharp1// Program.cs 2builder.Services.AddMediatR(cfg => 3 cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); 4 5// In a controller 6public async Task<IActionResult> CreateUser(CreateUserCommand command) 7{ 8 var result = await _mediator.Send(command); 9 return Ok(result); 10}
Benefits in Practice
1. Onboarding New Developers
New team members can quickly understand a feature by looking at a single folder.
2. Feature Evolution
When requirements change, all related code is in one place.
3. Microservices Preparation
Each vertical slice can potentially become a microservice.
4. Parallel Development
Multiple developers can work on different features without conflicts.
Common Concerns and Solutions
"What about code duplication?"
Some duplication is acceptable if it maintains feature independence. Extract to shared components only when there's genuine reuse across multiple features.
"How do I handle cross-cutting concerns?"
Use decorators, middleware, or attributes for logging, validation, and authorization that apply across features.
"What about database schema?"
You can still share a database schema while organizing application code vertically.
Migration Strategy
You don't need to rewrite your entire application. Start by:
- Identifying a single feature
- Creating a new vertical slice for it
- Moving related code into the slice
- Gradually migrating other features
Conclusion
Vertical Slice Architecture isn't about throwing away everything you know about software architecture. It's about organizing your code in a way that aligns with how your business thinks about features.
By focusing on business capabilities rather than technical layers, you'll find your code becomes more maintainable, your teams more productive, and your application more aligned with business needs.
Give it a try on your next feature – you might be surprised at how natural it feels!