Vertical Slice Architecture: Structuring Vertical Slices

Vertical Slice Architecture: Structuring Vertical Slices

Are you tired of organizing your project across layers?

Vertical Slice Architecture is a compelling alternative to traditional layered architectures. VSA flips the script on how we structure code.

Instead of horizontal layers (Presentation, Application, Domain), VSA organizes code by feature. Each feature encompasses everything it needs, from API endpoints to data access.

In this newsletter, we will explore how you can structure vertical slices in VSA.

Understanding Vertical Slices

At its core, a vertical slice represents a self-contained unit of functionality. It's a slice through the entire application stack. It encapsulates all the code and components necessary to fulfill a specific feature.

In traditional layered architectures, code is organized horizontally across the various layers. One feature implementation can be scattered across many layers. Changing a feature requires modifying the code in multiple layers.

VSA addresses this by grouping all the code for a feature into a single slice.

This shift in perspective brings several advantages:

  • Improved cohesion: Code related to a specific feature resides together, making it easier to understand, modify, and test.

  • Reduced complexity: VSA simplifies your application's mental model by avoiding the need to navigate multiple layers.

  • Focus on business logic: The structure naturally emphasizes the business use case over technical implementation details.

  • Easier maintenance: Changes to a feature are localized within its slice, reducing the risk of unintended side effects.

Implementing Vertical Slice Architecture

Here's an example vertical slice representing the CreateProduct feature. We use a static class to represent the feature and group the related types. Each feature can have a respective Request and Response class. The use case with the business logic can be in a Minimal API endpoint.

A vertical slice is likely either a command or a query. This approach gives us CQRS out of the box.

public static class CreateProduct
{
    public record Request(string Name, decimal Price);
    public record Response(int Id, string Name, decimal Price);

    public class Endpoint : IEndpoint
    {
        public void MapEndpoint(IEndpointRouteBuilder app)
        {
            app.MapPost("products", Handler).WithTags("Products");
        }

        public static IResult Handler(Request request, AppDbContext context)
        {
            var product = new Product
            {
                Name = request.Name,
                Price = request.Price
            };

            context.Products.Add(product);

            context.SaveChanges();

            return Results.Ok(
                new Response(product.Id, product.Name, product.Price));
        }
    }
}

I want to mention a few benefits of structuring your application like this.

The code for the entire CreateProduct feature is tightly grouped within a single file. This makes it extremely easy to locate, understand, and modify everything related to this functionality. We don't need to navigate multiple layers (like controllers, services, repositories, etc.).

Directly using AppDbContext within the endpoint might tightly couple the slice to your database technology. Depending on your project's size and requirements, you could consider abstracting data access (using a repository pattern) to make the slice more adaptable to changes in the persistence layer.

Introducing Validation in Vertical Slices

Vertical slices usually need to solve some cross-cutting concerns, one of which is validation. Validation is the gatekeeper, preventing invalid or malicious data from entering your system. We can easily implement validation with the FluentValidation library.

Within your slice, you'd define a Validator class that encapsulates the rules specific to your feature's request model. It also supports dependency injection, so we can run complex validations here.

public class Validator : AbstractValidator<Request>
{
    public Validator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Price).GreaterThanOrEqualTo(0);
        // ... other rules
    }
}

This validator can then be injected into your endpoint using dependency injection, allowing you to perform validation before processing the request.

public static class CreateProduct
{
    public record Request(string Name, decimal Price);
    public record Response(int Id, string Name, decimal Price);

    public class Validator : AbstractValidator<Request> // { ... }

    public class Endpoint : IEndpoint
    {
        public void MapEndpoint(IEndpointRouteBuilder app)
        {
            app.MapPost("products", Handler).WithTags("Products");
        }

        public static IResult Handler(
            Request request,
            IValidator<Request> validator,
            AppDbContext context)
        {
            var validationResult = await validator.Validate(request);
            if (!validationResult.IsValid)
            {
                return Results.BadRequest(validationResult.Errors);
            }

            // ... (Create product and return response)
        }
    }
}

Handling Complex Features and Shared Logic

The previous examples were simple. But what do we do with complex features and shared logic?

VSA excels at managing self-contained features. However, real-world applications often involve complex interactions and shared logic.

Here are a few strategies you can consider to address this:

  • Decomposition: Break down complex features into smaller, more manageable vertical slices. Each slice should represent a cohesive piece of the overall feature.

  • Refactoring: When a vertical slice becomes difficult to maintain, you can apply some refactoring techniques. The most common ones I use are Extract method and Extract class.

  • Extract shared logic: Identify common logic that's used across multiple features. Create a separate class (or extension method) to reference it from your vertical slices as needed.

  • Push logic down: Write vertical slices using procedural code, like a Transaction Script. Then, you can identify parts of the business logic that naturally belong to the domain entities.

You and your team will need to understand code smells and refactorings to make the most of VSA.

Summary

Vertical Slice Architecture is more than just a way to structure your code. By focusing on features, VSA allows you to create cohesive and maintainable applications. Vertical slices are self-contained, making unit and integration testing more straightforward.

VSA brings benefits in terms of code organization and development speed, making it a valuable tool in your toolbox. Code is grouped by feature, making it easier to locate and understand. The structure aligns with the way business users think about features. Changes are localized, reducing the risk of regressions and enabling faster iterations.

Consider embracing Vertical Slice Architecture in your next project. It's a big mindset shift from Clean Architecture. However, they both have their place and even share similar ideas.

That's all for this week. Stay awesome!


P.S. Whenever you’re ready, there are 3 ways I can help you:

  1. Modular Monolith Architecture (NEW): Join 600+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.

  2. Pragmatic Clean Architecture: Join 2,750+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.

  3. Patreon Community: Join a community of 1,050+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.