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.API/
├── Products/
│ ├── CreateProduct/
│ │ ├── CreateProductEndpoint.cs
│ │ └── CreateProductHandler.cs
│ ├── GetProducts/
│ │ ├── GetProductsEndpoint.cs
│ │ └── GetProductsHandler.cs
│ ├── GetProductById/
│ │ ├── GetProductByIdEndpoint.cs
│ │ └── GetProductByIdHandler.cs
│ ├── UpdateProduct/
│ └── DeleteProduct/
├── Models/
│ └── Product.cs
└── Program.cs
✅ All code for a feature in one place
✅ Easy to find and modify
✅ Changes are localized
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
Features are relatively independent
Quick iterations required
Example: Catalog service - managing products is straightforward CRUDLong-term maintainability critical
Multiple presentation layers
Need for maximum testability
Example: Ordering service - orders have complex state transitions, business rules, and domain events
Migration Path
Start with Vertical Slice
Begin new features with vertical slices for rapid development
Identify Complexity
Monitor features for growing complexity and business logic
Extract Domain Logic
When a feature becomes complex, extract domain logic into a separate layer
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:
Request → HTTP request DTO
Command/Query → CQRS object
Handler → Business logic
Result → Handler response
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