Vertical Slices in a Nutshell
I've moved to using the Vertical Slice Architecture for my applications and, after some initial experimentation, have found it to be a highly efficient method. This approach simplifies complex operations by treating each feature as a self-contained slice.
Over the past few months, I've taken a new direction in how I build my applications. I decided to try something called the Vertical Slice Architectural pattern. At first it was just an experiment, something new to learn. But now, after using it for a while, I've found it to be incredibly efficient. In this blog post, I want to share my journey with this approach, what it is, and why it might just change the way you think about building applications, too.
Think in Simple Interactions
At the heart of every human-machine interaction is a simple yet varied pattern: a request is made, it's processed in some way, and then a response is given. While this may seem like a broad description, it actually gets to the core of how we design our software systems.
Think of this whole process -from receiving a request to providing a response- as a single task. Looking at it this way not only helps us stay organized, it also makes complex operations easier to understand. In each of these transactions or pieces of work, there's an input (the request), something that handles the processing (the workhorse of the operation), and an output (the response). This structure helps us keep different aspects of our systems well-defined and ensures that they are both flexible and easy to adapt.
The idea is simple: organize the code into separated feature slices, with each slice containing a query or command, its handler, and the resulting response. The main objective is having high cohesion within each slice while minimizing coupling between slices. This approach proved quite effective for several aspects of the application architecture.
By taking a vertical view of the system, we increase its adaptability and minimize inter-dependencies. This approach allows us to select the most appropriate strategy or pattern for each specific scenario without affecting the implementation of existing scenarios.
High Cohesion within Feature Slices
Let's take a closer look at how I implemented the vertical slice architecture in my Location Services API as an example. The goal was to achieve a high level of cohesion within the slice, but to look at the slice in a very isolated way, which means that I packed all aspects of a feature, e.g. Register Customer, into a slice, as well as the API endpoint itself.
So I considered the endpoints to be integral parts of their slices and made sure that they were placed within those slices to maintain coherence. I've leveraged the powerful capabilities of .NET API controllers to reduce unnecessary code. I avoid traditional, overly complex controller classes - the kind that turn a "thing" like a fat, central CustomerController into a massive beast.
[ApiController]
[Route("api/v1/tenants/")]
public class RegisterCustomerEndpoint(
ISender mediator,
ILogger<RegisterCustomerEndpoint> logger) : ControllerBase
{
[HttpPost("{tenantId}/customers")]
public async Task<ActionResult> Post([FromBody]
CustomerRegistrationModel model, Guid tenantId)
{
try
{
var response = await mediator.Send(new RegisterCustomerCommand(
tenantId,
model.CustomerId,
model.DeviceId,
model.Name));
return CreatedAtAction(nameof(Post), response);
}
catch (CustomerAlreadyExistsException alreadyExistsException)
{
return Conflict(alreadyExistsException.Message);
}
catch (Exception exception)
{
logger. LogError(exception.Message);
return Problem(statusCode: 500);
}
}
}
So I decided that the controller should be placed directly in the appropriate feature slice folder and represent exactly one specific API endpoint.
The RegisterCustomerEndpoint class, derived from ControllerBase, contains all the necessary annotations and routing information. The endpoint is part of the slice, reinforcing the concept of cohesion.
Integrating endpoints directly into slices simplifies how to manage API endpoints. Now, each slice is like a little package that includes everything it needs - its endpoint, the logic to make it work, and how it handles data.
This approach also makes the code more cohesive. It's much easier to deal with and understand because all the parts that work together for a specific feature are kept together.
Using annotations in C# cuts down a lot of repetitive code. The code will be cleaner and simpler because you no longer need to manually add each endpoint to a router class.
I hope my insights inspire you to experiment with the Vertical Slice approach.