Vertical Slice Architecture: The ‘Clean Architecture’ Alternative That Actually Saves Time

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

The Layered Architecture Tax

Most developers start by learning the “N-Tier” stack. You know the pattern: Controllers talk to Services, Services talk to Repositories, and Repositories talk to the Database. For decades, this has been the default choice for keeping code “organized.”

However, I spent years fighting a consistent bottleneck in every enterprise project. Imagine you need to add a single ‘DiscountCode’ field to an order. In a layered project, you’re forced to perform what engineers call “Shotgun Surgery.” You open the Order entity, the OrderDTO, the OrderRepository, the IOrderService, the implementation, and finally the Controller. I once timed this; it took 15 minutes of tab-switching just to add one database column.

The code is decoupled by layer, but it’s tightly coupled by feature. This mismatch is why development feels sluggish. You spend more time navigating a 40-folder deep structure than writing business logic. Vertical Slice Architecture (VSA) fixes this by grouping code around “what it does” instead of “what it is technically.”

Moving to Slices in Under 10 Minutes

In a standard project, your folders look like a library index, scattering logic across the entire solution:

/Controllers
  - ProductController.cs
/Services
  - ProductService.cs
/Repositories
  - ProductRepository.cs
/Models
  - Product.cs
  - ProductDto.cs

In Vertical Slice Architecture, we stop grouping by technical role. Instead, we group by business capability. A quick refactor looks like this:

/Features
  /Products
    /GetProduct
      - GetProductEndpoint.cs
      - GetProductHandler.cs
      - GetProductResponse.cs
    /CreateProduct
      - CreateProductEndpoint.cs
      - CreateProductCommand.cs
      - CreateProductHandler.cs
      - CreateProductValidator.cs

Now, everything required to create a product lives in one place. If the business changes the creation rules, you open one folder. No more hunting through five different layers to find where the validation logic is hidden.

Why Slices Outperform Layers

Clean Architecture often relies on “just-in-case” abstractions. We create interfaces for every service because “we might swap the database.” In reality, I’ve seen teams spend 100+ hours maintaining interfaces for a database migration that never happened in five years. Business requirements, however, change every single week.

Building in slices allows each feature to be unique. If one feature is a basic CRUD operation, let the handler talk directly to the database using an ORM. If another involves complex pricing logic, use a rich domain model. You aren’t forced into a “one-size-fits-all” coffin for every API endpoint.

A Practical Python/FastAPI Example

Instead of a bloated 2,000-line services.py, we create a specific file for the feature. Here is a slice for updating a user profile:

# features/users/update_profile.py
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from sqlalchemy.orm import Session
from db import get_db, User

router = APIRouter()

class UpdateProfileRequest(BaseModel):
    display_name: str
    bio: str

@router.put("/users/me")
def handle_update_profile(request: UpdateProfileRequest, db: Session = Depends(get_db)):
    # Logic, validation, and persistence happen right here.
    user = db.query(User).filter(User.id == current_user_id).first()
    user.display_name = request.display_name
    user.bio = request.bio
    db.commit()
    return {"status": "success"}

By putting the schema and logic in one file, I’ve seen teams reduce their “time-to-first-commit” for new features by nearly 40%. You keep the entire context of the change in your head without jumping between 12 open tabs.

Handling Shared Logic Without the Mess

The first question I always get is: “Don’t I end up repeating myself?” If two slices both need to calculate a specific tax rate, you don’t copy-paste the code.

The solution is to move truly shared rules into a Domain or Shared folder. But here is the catch: don’t share code just because it looks similar today. Two features might look identical on Monday but evolve into completely different animals by Friday. In VSA, we prefer a few lines of duplication over a brittle, “god-object” abstraction that breaks everything when it changes.

Using the Mediator Pattern (C#)

For larger projects, the Mediator pattern (like the MediatR library) keeps controllers thin. The controller simply broadcasts a message, and the specific handler in the slice picks it up.

// Features/Orders/CreateOrder/CreateOrderHandler.cs
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly AppDbContext _db;
    public CreateOrderHandler(AppDbContext db) => _db = db;

    public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken ct)
    {
        var order = new Order(request.CustomerId);
        _db.Orders.Add(order);
        await _db.SaveChangesAsync(ct);
        return order.Id;
    }
}

Actionable Tips for Your Next Sprint

  • Start Small: Don’t refactor the whole monolith. Create a /Features folder and build your next ticket as a vertical slice.
  • Enforce Boundaries: Slices shouldn’t call each other directly. Use events or shared domain models if they need to communicate.
  • Delete the Repository: If a slice only needs one SQL query, just write the query in the handler. You don’t need a three-layer abstraction for a simple SELECT statement.
  • Refactor by Split: If a folder like /Orders gets crowded, split it. /Orders/Cancel and /Orders/Refund are much easier to manage than one massive OrderService.

Switching from layers to features lowers your cognitive load. It might feel “messy” to see a database query next to a validation rule, but the productivity boost is undeniable. You’re no longer a folder navigator; you’re a feature builder who ships faster.

Share: