Embracing Vertical Slices Beyond N-Tier Architectures
James Reed
Infrastructure Engineer · Leapcell

The Monolithic Divide Rethinking Application Structures
In the ever-evolving landscape of software development, the way we structure our applications profoundly impacts their maintainability, scalability, and developer experience. For decades, the N-tier architecture, with its distinct layers for presentation, business logic, and data access, has been the de facto standard. While offering clear separation of concerns, this approach often leads to horizontal coupling – changes in one part of a layer can ripple across the entire application, making development cumbersome and deployment risky, especially as applications grow complex. This struggle prompts us to question whether the traditional N-tier model truly serves the needs of today's agile development environments. This article introduces an alternative paradigm, the "Vertical Slice Architecture," as a compelling solution for building more cohesive and manageable applications, particularly within the contexts of ASP.NET Core and FastAPI.
Decoding Vertical Slices and Their Core Tenets
Before diving into the practicalities, let's clarify the key concepts underpinning vertical slice architecture.
N-Tier Architecture: This is a traditional architectural pattern where an application is divided into logical layers, such as presentation, business logic (service layer), and data access (repository layer). Each layer has a specific responsibility, and communication typically flows unidirectionally between them.
Vertical Slice: Unlike N-tier's horizontal slicing of concerns, a vertical slice encapsulates all the components necessary to deliver a single feature or use case end-to-end. This includes its API endpoint, business logic, data access, and even its specific UI components (though this article primarily focuses on the backend). Each slice is independent and can often be developed, tested, and deployed in isolation.
Domain-Driven Design (DDD): While not strictly required, vertical slicing often aligns well with DDD principles, where features are organized around business domains. This naturally leads to cohesive slices representing specific domain capabilities.
Clean Architecture / Hexagonal Architecture: These architectures emphasize separating concerns based on their distance from the core business logic. Vertical slices can be seen as a practical implementation strategy within these architectural styles, allowing each slice to adhere to these principles independently.
The core principle of vertical slicing is to prioritize cohesion per feature over cohesion per technical concern. Instead of having a single UserService that handles all user-related operations, you might have separate slices for "Create User," "Get User Details," and "Update User Profile." Each slice is a miniature, self-contained application, dramatically reducing coupling between unrelated features.
Implementing Vertical Slices in ASP.NET Core
Let's illustrate vertical slicing with a simple ASP.NET Core application managing products. Instead of a ProductService and ProductRepository, we'll create separate slices for CreateProduct, GetProductById, and ListProducts. We'll leverage MediatR for handling requests and commands, which naturally fits the vertical slice pattern by routing requests to specific handlers.
First, install MediatR:
dotnet add package MediatR dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
Consider a CreateProduct feature. Its entire scope, from endpoint to database, resides within a single, dedicated folder.
// Features/Products/CreateProduct.cs using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Threading; using System.Threading.Tasks; namespace MyWebApp.Features.Products { public static class CreateProduct { // 1. Command (Input) public class Command : IRequest<Response> { public string Name { get; set; } public decimal Price { get; set; } } // 2. Response (Output) public class Response { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } // 3. Handler (Business Logic & Data Access) public class Handler : IRequestHandler<Command, Response> { private readonly ProductContext _context; public Handler(ProductContext context) { _context = context; } public async Task<Response> Handle(Command request, CancellationToken cancellationToken) { var product = new Product { Name = request.Name, Price = request.Price }; _context.Products.Add(product); await _context.SaveChangesAsync(cancellationToken); return new Response { Id = product.Id, Name = product.Name, Price = product.Price }; } } // 4. API Endpoint (Controller) [ApiController] [Route("api/products")] public class ProductsController : ControllerBase { private readonly IMediator _mediator; public ProductsController(IMediator mediator) { _mediator = mediator; } [HttpPost] public async Task<ActionResult<Response>> Post(Command command) { var response = await _mediator.Send(command); return CreatedAtAction(nameof(Post), new { id = response.Id }, response); } } } // Shared: A simple Entity Framework Core DbContext and Product entity public class Product { public int Id { get; set; } public string Name { get; set; } public decimal Price { get; set; } } public class ProductContext : DbContext { public DbSet<Product> Products { get; set; } public ProductContext(DbContextOptions<ProductContext> options) : base(options) { } } }
In Program.cs or Startup.cs, configure MediatR and your DbContext:
// Program.cs using Microsoft.EntityFrameworkCore; using MediatR; using MyWebApp.Features.Products; // Important for reflection var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddDbContext<ProductContext>(options => options.UseInMemoryDatabase("ProductDb")); // Using in-memory for simplicity builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); // Scan for MediatR handlers var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
Notice how CreateProduct.cs contains almost everything related to that specific feature. The controller acts as a thin facade, dispatching the command to the MediatR pipeline.
Implementing Vertical Slices in FastAPI
FastAPI, with its strong emphasis on Pydantic models and dependency injection, also lends itself beautifully to vertical slicing. We can achieve a similar structure by defining our feature's routes, models, and logic within a dedicated module or directory.
# app/features/products/create_product.py from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel from typing import Optional # Assume a simple in-memory "database" for demonstration # In a real app, this would be an ORM like SQLAlchemy interacting with a database class ProductDB: def __init__(self): self.products = [] self.next_id = 1 def create_product(self, name: str, price: float): product_data = {"id": self.next_id, "name": name, "price": price} self.products.append(product_data) self.next_id += 1 return product_data def get_product(self, product_id: int): for product in self.products: if product["id"] == product_id: return product return None # Dependency to provide a database instance (can be swapped for a real DB session) def get_db(): return ProductDB() # 1. Request Body Model class CreateProductRequest(BaseModel): name: str price: float # 2. Response Model class ProductResponse(BaseModel): id: int name: str price: float # 3. Router (API Endpoint & Business Logic) router = APIRouter(prefix="/products", tags=["products"]) @router.post("/", response_model=ProductResponse, status_code=status.HTTP_201_CREATED) async def create_product_endpoint( request: CreateProductRequest, db: ProductDB = Depends(get_db) # Inject our "database" ): """ Creates a new product. """ created_product_data = db.create_product(request.name, request.price) return ProductResponse(**created_product_data) @router.get("/{product_id}", response_model=ProductResponse) async def get_product_endpoint( product_id: int, db: ProductDB = Depends(get_db) ): """ Retrieves a product by its ID. """ product = db.get_product(product_id) if product is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Product not found") return ProductResponse(**product)
In app/main.py:
# app/main.py from fastapi import FastAPI from .features.products import create_product app = FastAPI(title="Vertical Slice Product API") # Include the feature-specific router app.include_router(create_product.router) @app.get("/") async def root(): return {"message": "Welcome to the Vertical Slice API!"}
Here, create_product.py encapsulates its own models, router (acting as the endpoint and handling business logic), and even its specific "database" interaction. Dependency injection (Depends(get_db)) within FastAPI ensures that each feature can declare its specific dependencies without affecting others.
When to Consider Vertical Slices
Vertical slice architecture shines in several scenarios:
- Growing Monoliths: When an N-tier application becomes difficult to navigate and modify, vertical slicing can help isolate new features and gradually refactor existing ones into slices.
 - Microservices Transition: It can serve as an excellent stepping stone towards microservices, as each vertical slice is a candidate for extraction into a separate service later on.
 - Feature-Driven Development: Teams that organize around features rather than technical layers will find this pattern naturally aligned with their workflow.
 - Smaller Teams: By reducing cognitive load and limiting the blast radius of changes, small teams can develop and deploy features more independently.
 - Highly Iterated Products: When features are frequently added, changed, or removed, the isolation provided by vertical slices makes these operations safer and faster.
 
However, it's not a silver bullet. For very small, simple applications with minimal complexity, the overhead of setting up and adhering to vertical slices might not be necessary. Shared core logic or cross-cutting concerns (like authentication, logging) still need a thoughtful design, often using middleware or pipelines that affect all slices.
Uniting Cohesion and Agility
The vertical slice architecture challenges the long-held tradition of N-tier design by shifting our focus from technical layering to feature-centric cohesion. By bringing all components related to a specific use case into a single unit, developers can achieve greater autonomy, reduce accidental coupling, and accelerate the development of complex applications in frameworks like ASP.NET Core and FastAPI. Embrace vertical slices to build applications that are not just functional, but also incredibly agile and maintainable, paving the way for more resilient and scalable software systems.

