Skip to main content

What is Vertical Slice Architecture?

Vertical Slice Architecture organizes code by features rather than technical layers. Each feature (or “slice”) contains all the code needed for that specific functionality, from API endpoint to database access.
Implementation: The Catalog service demonstrates Vertical Slice ArchitectureLocation: src/Services/Catalog/Catalog.API/

Traditional Layers vs Vertical Slices

Catalog.API/
├── Controllers/
│   ├── ProductController.cs
│   └── CategoryController.cs
├── Services/
│   ├── ProductService.cs
│   └── CategoryService.cs
├── Repositories/
│   ├── ProductRepository.cs
│   └── CategoryRepository.cs
├── Models/
│   ├── Product.cs
│   └── Category.cs
└── DTOs/
    ├── ProductDto.cs
    └── CategoryDto.cs

❌ Scattered across many folders
❌ Hard to find all code for a feature
❌ Changes affect multiple layers

Catalog Service Structure

Anatomy of a Vertical Slice

Let’s examine the CreateProduct feature:

File Structure

Products/CreateProduct/
├── CreateProductEndpoint.cs    # HTTP endpoint (API layer)
└── CreateProductHandler.cs     # Business logic (Handler)

1. Endpoint Definition

src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductEndpoint.cs
namespace Catalog.API.Products.CreateProduct;

// Request contract
public record CreateProductRequest(
    string Name, 
    List<string> Category, 
    string Description, 
    string ImageFile, 
    decimal Price);

// Response contract
public record CreateProductResponse(Guid Id);

public class CreateProductEndpoint : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPost("/products",
            async (CreateProductRequest request, ISender sender) =>
        {
            // 1. Map request to command
            var command = request.Adapt<CreateProductCommand>();

            // 2. Send command via MediatR
            var result = await sender.Send(command);

            // 3. Map result to response
            var response = result.Adapt<CreateProductResponse>();

            return Results.Created($"/products/{response.Id}", response);
        })
        .WithName("CreateProduct")
        .Produces<CreateProductResponse>(StatusCodes.Status201Created)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .WithSummary("Create Product")
        .WithDescription("Create Product");
    }
}
Carter Module: Uses Carter library for clean endpoint organization. Each endpoint is a module that registers routes.

2. Handler Implementation

src/Services/Catalog/Catalog.API/Products/CreateProduct/CreateProductHandler.cs
namespace Catalog.API.Products.CreateProduct;

// Command using CQRS pattern
public record CreateProductCommand(
    string Name, 
    List<string> Category, 
    string Description, 
    string ImageFile, 
    decimal Price)
    : ICommand<CreateProductResult>;

// Result
public record CreateProductResult(Guid Id);

// Validation rules
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
    public CreateProductCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty().WithMessage("Name is required");
        RuleFor(x => x.Category).NotEmpty().WithMessage("Category is required");
        RuleFor(x => x.ImageFile).NotEmpty().WithMessage("ImageFile is required");
        RuleFor(x => x.Price).GreaterThan(0).WithMessage("Price must be greater than 0");
    }
}

// Handler with business logic
internal class CreateProductCommandHandler
    (IDocumentSession session)
    : ICommandHandler<CreateProductCommand, CreateProductResult>
{
    public async Task<CreateProductResult> Handle(
        CreateProductCommand command, 
        CancellationToken cancellationToken)
    {
        // Create Product entity from command object
        var product = new Product
        {
            Name = command.Name,
            Category = command.Category,
            Description = command.Description,
            ImageFile = command.ImageFile,
            Price = command.Price
        };
        
        // Save to database using Marten
        session.Store(product);
        await session.SaveChangesAsync(cancellationToken);

        // Return result
        return new CreateProductResult(product.Id);
    }
}
Everything related to creating a product is in one folder: endpoint, command, validation, and handler.

Complete Feature Examples

GetProducts Query

namespace Catalog.API.Products.GetProducts;

public record GetProductsRequest(int? PageNumber = 1, int? PageSize = 10);
public record GetProductsResponse(IEnumerable<Product> Products);

public class GetProductsEndpoint : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapGet("/products", 
            async ([AsParameters] GetProductsRequest request, ISender sender) =>
        {
            var query = request.Adapt<GetProductsQuery>();
            var result = await sender.Send(query);
            var response = result.Adapt<GetProductsResponse>();
            return Results.Ok(response);
        })
        .WithName("GetProducts")
        .Produces<GetProductsResponse>(StatusCodes.Status200OK)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .WithSummary("Get Products")
        .WithDescription("Get Products");
    }
}

UpdateProduct Command

namespace Catalog.API.Products.UpdateProduct;

public record UpdateProductRequest(
    Guid Id,
    string Name,
    List<string> Category,
    string Description,
    string ImageFile,
    decimal Price);

public record UpdateProductResponse(bool IsSuccess);

public class UpdateProductEndpoint : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPut("/products",
            async (UpdateProductRequest request, ISender sender) =>
        {
            var command = request.Adapt<UpdateProductCommand>();
            var result = await sender.Send(command);
            var response = result.Adapt<UpdateProductResponse>();
            return Results.Ok(response);
        })
        .WithName("UpdateProduct")
        .Produces<UpdateProductResponse>(StatusCodes.Status200OK)
        .ProducesProblem(StatusCodes.Status400BadRequest)
        .WithSummary("Update Product")
        .WithDescription("Update Product");
    }
}

Simple Domain Model

The Catalog service uses a simple entity without DDD complexity:
src/Services/Catalog/Catalog.API/Models/Product.cs
namespace Catalog.API.Models;

public class Product
{
    public Guid Id { get; set; }
    public string Name { get; set; } = default!;
    public List<string> Category { get; set; } = new();
    public string Description { get; set; } = default!;
    public string ImageFile { get; set; } = default!;
    public decimal Price { get; set; }
}
Simple Model Characteristics:
  • Public setters (no encapsulation needed)
  • No value objects
  • No domain events
  • No business logic in the model
  • Suitable for CRUD operations

Technology Stack

The Catalog service uses:

Marten

PostgreSQL as a document database
  • NoSQL-like API
  • Strong consistency
  • LINQ support

MediatR

CQRS pattern implementation
  • Commands and Queries
  • Pipeline behaviors
  • Validation and logging

Carter

Minimal API organization
  • Feature-based routing
  • Clean endpoint definition
  • OpenAPI support

FluentValidation

Input validation
  • Declarative validation rules
  • Automatic integration
  • Clear error messages

Program.cs Configuration

src/Services/Catalog/Catalog.API/Program.cs
using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;

var builder = WebApplication.CreateBuilder(args);

// Register services
var assembly = typeof(Program).Assembly;

// MediatR with CQRS behaviors
builder.Services.AddMediatR(config =>
{
    config.RegisterServicesFromAssembly(assembly);
    config.AddOpenBehavior(typeof(ValidationBehavior<,>));
    config.AddOpenBehavior(typeof(LoggingBehavior<,>));
});

// FluentValidation
builder.Services.AddValidatorsFromAssembly(assembly);

// Carter for endpoints
builder.Services.AddCarter();

// Marten for PostgreSQL document store
builder.Services.AddMarten(opts =>
{
    opts.Connection(builder.Configuration.GetConnectionString("Database")!);
}).UseLightweightSessions();

// Seed data in development
if (builder.Environment.IsDevelopment())
    builder.Services.InitializeMartenWith<CatalogInitialData>();

// Exception handling
builder.Services.AddExceptionHandler<CustomExceptionHandler>();

// Health checks
builder.Services.AddHealthChecks()
    .AddNpgSql(builder.Configuration.GetConnectionString("Database")!);

var app = builder.Build();

// Configure HTTP pipeline
app.MapCarter();  // Registers all Carter modules

app.UseExceptionHandler(options => { });

app.UseHealthChecks("/health",
    new HealthCheckOptions
    {
        ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
    });

app.Run();

Benefits of Vertical Slice Architecture

Cohesion

Related code is in one place, making it easier to understand and modify

Low Coupling

Features are independent, changes don’t ripple across layers

Quick Development

Add new features by copying and modifying existing slices

Easy Navigation

Find everything for a feature in one folder

Team Scalability

Multiple developers can work on different features without conflicts

Flexibility

Each feature can use different patterns if needed

Comparison: Vertical Slice vs Clean Architecture

Simple to moderate business logic
CRUD-heavy applications
Rapid development needed
Small to medium teams
Features are relatively independent
Quick iterations required
Example: Catalog service - managing products is straightforward CRUD

Migration Path

1

Start with Vertical Slice

Begin new features with vertical slices for rapid development
2

Identify Complexity

Monitor features for growing complexity and business logic
3

Extract Domain Logic

When a feature becomes complex, extract domain logic into a separate layer
4

Evolve to Clean Architecture

Gradually introduce Clean Architecture patterns for complex domains
You don’t have to choose one pattern for the entire application. Use Vertical Slice for simple features and Clean Architecture for complex ones, just like AspNetRun does!

Common Patterns in Vertical Slices

Pattern 1: CQRS

All slices use CQRS to separate reads and writes:
  • Commands: CreateProduct, UpdateProduct, DeleteProduct
  • Queries: GetProducts, GetProductById, GetProductByCategory

Pattern 2: Minimal APIs with Carter

Each feature defines its routes using Carter modules:
public class CreateProductEndpoint : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPost("/products", async (request, sender) => { });
    }
}

Pattern 3: Request-Handler-Response

Consistent flow across all features:
  1. Request → HTTP request DTO
  2. Command/Query → CQRS object
  3. Handler → Business logic
  4. Result → Handler response
  5. Response → HTTP response DTO

Pattern 4: Validation

FluentValidation validators in each feature:
public class CreateProductCommandValidator : AbstractValidator<CreateProductCommand>
{
    public CreateProductCommandValidator()
    {
        RuleFor(x => x.Name).NotEmpty();
        RuleFor(x => x.Price).GreaterThan(0);
    }
}

Clean Architecture

Compare with the layered approach in Ordering service

CQRS Pattern

Understand CQRS used in vertical slices

Microservices

See how Catalog service fits in the microservices architecture

DDD Principles

Learn when to evolve from simple models to DDD

Build docs developers (and LLMs) love