Architecture Pattern

Vertical Slice Architecture

Organize code by feature, not by technical layer.

Vertical Slice Architecture: A Complete Guide with Examples in 11 Programming Languages

Vertical Slice Architecture — popularized by Jimmy Bogard — organizes an application by feature rather than by layer. Instead of grouping all controllers together, all services together, and all repositories together in horizontal layers, each feature owns its complete slice of the stack from the HTTP endpoint down to the database query. A "vertical slice" for "Place Order" contains everything that feature needs: the HTTP controller, the request/response models, the validation, the business logic, the database access, and the mapping. It is entirely self-contained. Adding a new feature means adding a new slice — not touching five existing layers.

In this comprehensive guide, we'll explore Vertical Slice Architecture with real-world examples in Python, TypeScript, Java, JavaScript, C#, PHP, Go, Rust, Dart, Swift, and Kotlin — complete with language-specific best practices, common pitfalls, and when to use this powerful pattern.

Table of Contents

What is Vertical Slice Architecture?

Vertical Slice Architecture challenges one of the most deeply held assumptions in software design: that the right way to organize code is by technical role — all controllers in one place, all services in another, all repositories in a third.

The layered approach feels logical. But it creates a hidden cost that grows with every feature: any single user-facing action touches every layer. Adding "Create Product" requires modifying ProductController, ProductService, ProductRepository, ProductDTO, and ProductMapper — five files in five different directories, often in five different team members' domains. The feature is scattered horizontally across the codebase. Understanding what "Create Product" does requires reading five files, navigating five directories, and holding the entire stack in your head simultaneously.

Vertical Slice Architecture inverts this organization. Instead of grouping by technical role, it groups by feature. All the code for "Create Product" — controller, validation, business logic, database query, response mapping — lives together in one place: features/create-product/. A developer reading the feature reads one file or one small directory. A developer adding a feature adds one new slice. A developer deleting a feature deletes one directory.

The key insight is: features are the axis of change, not technical layers. When requirements change for "Create Product", every change is in one place. When "Delete Product" needs to be added, it is added in its own isolated slice with no risk of breaking "Create Product". The coupling that matters — business coupling within a feature — is explicit and co-located. The coupling that hurts — incidental coupling between unrelated features through shared layers — is eliminated.

Think of a department store. A traditional layered architecture is like organizing the store by item type: all clothing on floor 1, all electronics on floor 2, all furniture on floor 3. When you want to outfit a home office, you must visit every floor. Vertical Slice Architecture is like organizing the store by life scenario: the "home office" section has clothing, electronics, and furniture all together. Each scenario is self-contained.

Why Use Vertical Slice Architecture?

Vertical Slice Architecture offers several compelling benefits:

  1. Feature Cohesion: All code related to a single feature is co-located — understanding, modifying, testing, and deleting a feature requires touching one place rather than five scattered layers
  2. Reduced Coupling Between Features: Features do not share service classes or repository classes — they share only what is genuinely shared (database schema, domain entities). Changing one feature carries no risk of breaking another
  3. Independent Scalability: Each slice can use the pattern most appropriate for its complexity — a simple CRUD slice needs no domain objects; a complex business feature can use a rich domain model, all within the same codebase
  4. Faster Onboarding: A new developer can understand and contribute to a single feature by reading one slice — they do not need to understand the entire horizontal layer structure before making their first change
  5. Parallel Team Development: Different teams or developers own different slices with no merge conflicts — team A modifies "Place Order" while team B modifies "Process Refund" with zero interference
  6. Screaming Architecture: The folder structure of a Vertical Slice codebase screams its business capabilities — features/place-order/, features/cancel-subscription/, features/generate-invoice/ — rather than screaming its technical implementation (controllers/, services/, repositories/)

Vertical Slice Architecture Comparison

Let's compare Vertical Slice Architecture with related architectural styles:

ArchitectureOrganized ByFeature CohesionCross-Feature CouplingBest For
Vertical SliceFeature / Use CaseMaximum — entire feature in one placeMinimum — slices share only domain coreFeature-rich apps, multiple teams, CQRS
Layered (N-Tier)Technical RoleLow — feature spread across 5+ filesMedium — shared service classesFamiliar teams, simpler domains
Hexagonal (Ports & Adapters)Domain IsolationMedium — use cases isolated, infra sharedLow — ports separate concernsComplex domain, multiple channels
OnionConcentric RingsMedium — use cases in one ringLow — rings enforce separationDDD, long-lived domain isolation
MicroservicesBusiness CapabilityMaximum per serviceNone — services fully independentLarge scale, independent deployability

Key Distinctions:

  • Vertical Slice vs. Layered Architecture: Layered organizes by what kind of code it is (controller, service, repository). Vertical Slice organizes by what business problem it solves (place order, cancel subscription). A single feature is spread across multiple directories in Layered; it is in one directory in Vertical Slice. The mental model shift is from "where does this type of code belong?" to "which feature does this code serve?"
  • Vertical Slice vs. Hexagonal/Onion: Hexagonal and Onion are about protecting the domain from infrastructure by enforcing inward dependencies. Vertical Slice is about feature cohesion — grouping all the code for one feature together regardless of which ring or layer it would belong to. The two approaches are complementary: you can use Hexagonal principles within each slice while organizing across slices vertically.
  • Vertical Slice vs. Microservices: Microservices achieve feature isolation through deployment boundaries — each service is independently deployable. Vertical Slice achieves feature isolation through code organization boundaries within a single deployable application. Vertical Slice is "microservices-like thinking inside a monolith" — the same principle of feature ownership and isolation, without the operational overhead of distributed services.

Vertical Slice Architecture Explained

Vertical Slice Architecture organizes around three concepts:

Core Concepts

  1. Feature Slice: The fundamental unit of organization. A slice represents one discrete user-facing capability — "Get Product", "Create Order", "Cancel Subscription", "Generate Invoice". Each slice contains everything it needs to fulfill that capability from input to output. A slice is typically a single file or a small, tightly focused directory. Slices are intentionally not shared — code duplication between slices is acceptable and often preferable to introducing cross-slice coupling.

  2. Shared Kernel: The small, stable set of code genuinely shared across slices. This includes domain entities and value objects (the Product, Order, Customer data model), common infrastructure (database connection, middleware, authentication), and cross-cutting utilities (logging, error handling, pagination helpers). The Shared Kernel is carefully curated — only things that truly belong to all slices. When something is added to the Shared Kernel, it affects every slice. Keep it minimal.

  3. Mediator Pattern (optional but common): Many Vertical Slice implementations use a Mediator (MediatR in .NET, a custom dispatcher in other languages) to decouple the HTTP layer from feature handlers. The controller sends a CreateProductCommand or GetProductQuery to the Mediator, which routes it to the appropriate handler (the slice). This makes slices fully self-contained — the controller knows only the command/query; the handler knows only the command/query and the database.

Command Query Responsibility Segregation (CQRS) Alignment

Vertical Slice Architecture aligns naturally with CQRS. Every slice is either a Command (write operation that changes state: Create, Update, Delete) or a Query (read operation that returns data without changing state: Get, List, Search). Commands and queries have different needs:

  • Commands need validation, business rules, domain events, and transactional writes
  • Queries need efficient reads, often bypassing the domain model entirely for performance

Each slice chooses the right approach for its type. A simple GetProduct query might query the database directly and return a DTO — no domain model involved. A complex PlaceOrder command might load domain aggregates, invoke business rules, publish events, and commit a transaction — full DDD treatment. The same codebase, different complexity per feature.

Slice Variants

  • Single-file slice: The entire feature — handler, validation, database query, response mapping — in one file. Extreme cohesion; works well for small, focused features
  • Folder-per-slice: A directory per feature containing handler.py, command.py, validator.py, response.py. Allows slices to grow while remaining co-located
  • CQRS slices: Explicit separation of commands (create_product/command.py, create_product/handler.py) and queries (get_product/query.py, get_product/handler.py) within the feature folder
  • Slice with inner layers: Large, complex features use Hexagonal or Onion internally within their slice — the slice boundary is the outer face; the inner structure uses whatever pattern fits the complexity

Diagram

Here is the canonical Vertical Slice Architecture diagram:

  TRADITIONAL LAYERED                    VERTICAL SLICE
  ─────────────────────                  ─────────────────────────────────────

  ┌────────────────────┐                 ┌──────────────────────────────────┐
  │    CONTROLLERS     │                 │           FEATURE SLICES         │
  │ ProductController  │                 ├──────────┬──────────┬────────────┤
  │ OrderController    │                 │ Create   │  Get     │  Update    │
  │ UserController     │                 │ Product  │  Product │  Product   │
  └────────────────────┘                 │          │          │            │
  ┌────────────────────┐                 │ handler  │ handler  │ handler    │
  │     SERVICES       │                 │ command  │ query    │ command    │
  │ ProductService     │                 │ validate │ response │ validate   │
  │ OrderService       │                 │ db query │ db query │ db query   │
  │ UserService        │                 └──────────┴──────────┴────────────┤
  └────────────────────┘                 ├──────────┬──────────┬────────────┤
  ┌────────────────────┐                 │  Place   │ Cancel   │  List      │
  │   REPOSITORIES     │                 │  Order   │  Order   │  Orders    │
  │ ProductRepository  │                 │          │          │            │
  │ OrderRepository    │                 │ handler  │ handler  │ handler    │
  │ UserRepository     │                 │ domain   │ domain   │ query      │
  └────────────────────┘                 │ db write │ db write │ db read    │
                                         └──────────┴──────────┴────────────┤
  Feature "Place Order"                  ├──────────────────────────────────┤
  touches ALL layers                     │         SHARED KERNEL            │
                                         │  Domain Entities · DB Connection │
  Feature "Create Product"               │  Auth Middleware · Error Handler │
  touches ALL layers                     └──────────────────────────────────┘

Beginner-Friendly Example

Let's build a product catalog API using Vertical Slice Architecture. Each API operation — Create Product, Get Product, List Products, Update Product — is its own self-contained slice. The slices share a Product entity and a database connection from the Shared Kernel, but their handlers, validation, and response mapping are completely independent.

from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional
import uuid

# ═══════════════════════════════════════════════════════════════════════════════
# SHARED KERNEL — domain entities and infrastructure shared by all slices
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class Product:
    """Shared domain entity — used by all product slices."""
    id: str
    name: str
    description: str
    price: float
    stock: int
    created_at: datetime = field(default_factory=datetime.utcnow)
    updated_at: datetime = field(default_factory=datetime.utcnow)

    def __post_init__(self) -> None:
        if not self.name.strip():
            raise ValueError("Product name cannot be blank")
        if self.price < 0:
            raise ValueError("Price cannot be negative")
        if self.stock < 0:
            raise ValueError("Stock cannot be negative")


class InMemoryDatabase:
    """Shared infrastructure — the database connection/store."""
    def __init__(self) -> None:
        self._products: dict[str, Product] = {}

    @property
    def products(self) -> dict[str, Product]:
        return self._products


# ═══════════════════════════════════════════════════════════════════════════════
# SLICE: Create Product
# — everything for creating a product lives here, and only here
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class CreateProductCommand:
    name: str
    description: str
    price: float
    stock: int


@dataclass
class CreateProductResponse:
    id: str
    name: str
    price: float
    stock: int
    created_at: str


class CreateProductHandler:
    def __init__(self, db: InMemoryDatabase) -> None:
        self._db = db

    def handle(self, cmd: CreateProductCommand) -> CreateProductResponse:
        # Slice-specific validation
        if not cmd.name.strip():
            raise ValueError("Product name is required")
        if cmd.price < 0:
            raise ValueError("Price must be non-negative")
        if cmd.stock < 0:
            raise ValueError("Stock must be non-negative")

        # Create domain entity
        product = Product(
            id=str(uuid.uuid4()),
            name=cmd.name.strip(),
            description=cmd.description,
            price=cmd.price,
            stock=cmd.stock,
        )

        # Persist — this slice owns its own data access logic
        self._db.products[product.id] = product

        # Map to response — this slice owns its own response shape
        return CreateProductResponse(
            id=product.id,
            name=product.name,
            price=product.price,
            stock=product.stock,
            created_at=product.created_at.isoformat(),
        )


# ═══════════════════════════════════════════════════════════════════════════════
# SLICE: Get Product
# — everything for fetching a single product lives here
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class GetProductQuery:
    product_id: str


@dataclass
class GetProductResponse:
    id: str
    name: str
    description: str
    price: float
    stock: int
    created_at: str


class GetProductHandler:
    def __init__(self, db: InMemoryDatabase) -> None:
        self._db = db

    def handle(self, query: GetProductQuery) -> GetProductResponse:
        product = self._db.products.get(query.product_id)
        if product is None:
            raise ValueError(f"Product '{query.product_id}' not found")

        # This slice's response includes description — Create's doesn't
        return GetProductResponse(
            id=product.id,
            name=product.name,
            description=product.description,
            price=product.price,
            stock=product.stock,
            created_at=product.created_at.isoformat(),
        )


# ═══════════════════════════════════════════════════════════════════════════════
# SLICE: List Products
# — everything for listing products lives here
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class ListProductsQuery:
    min_price: Optional[float] = None
    in_stock_only: bool = False


@dataclass
class ProductSummary:
    id: str
    name: str
    price: float
    in_stock: bool


@dataclass
class ListProductsResponse:
    items: list[ProductSummary]
    total: int


class ListProductsHandler:
    def __init__(self, db: InMemoryDatabase) -> None:
        self._db = db

    def handle(self, query: ListProductsQuery) -> ListProductsResponse:
        products = list(self._db.products.values())

        # Slice-specific filtering logic — not shared with other slices
        if query.min_price is not None:
            products = [p for p in products if p.price >= query.min_price]
        if query.in_stock_only:
            products = [p for p in products if p.stock > 0]

        items = [
            ProductSummary(
                id=p.id,
                name=p.name,
                price=p.price,
                in_stock=p.stock > 0,
            )
            for p in products
        ]
        return ListProductsResponse(items=items, total=len(items))


# ═══════════════════════════════════════════════════════════════════════════════
# SLICE: Update Product Price
# — everything for updating a product's price lives here
# ═══════════════════════════════════════════════════════════════════════════════

@dataclass
class UpdateProductPriceCommand:
    product_id: str
    new_price: float
    reason: str


@dataclass
class UpdateProductPriceResponse:
    id: str
    name: str
    old_price: float
    new_price: float
    updated_at: str


class UpdateProductPriceHandler:
    def __init__(self, db: InMemoryDatabase) -> None:
        self._db = db

    def handle(self, cmd: UpdateProductPriceCommand) -> UpdateProductPriceResponse:
        product = self._db.products.get(cmd.product_id)
        if product is None:
            raise ValueError(f"Product '{cmd.product_id}' not found")
        if cmd.new_price < 0:
            raise ValueError("New price cannot be negative")
        if not cmd.reason.strip():
            raise ValueError("Price change reason is required")

        old_price = product.price
        product.price = cmd.new_price
        product.updated_at = datetime.utcnow()

        return UpdateProductPriceResponse(
            id=product.id,
            name=product.name,
            old_price=old_price,
            new_price=product.price,
            updated_at=product.updated_at.isoformat(),
        )


# ═══════════════════════════════════════════════════════════════════════════════
# HTTP LAYER — thin routing layer that dispatches to slices
# ═══════════════════════════════════════════════════════════════════════════════

class ProductAPI:
    """Thin HTTP-like dispatcher that routes requests to the appropriate slice."""

    def __init__(self, db: InMemoryDatabase) -> None:
        self._create_handler  = CreateProductHandler(db)
        self._get_handler     = GetProductHandler(db)
        self._list_handler    = ListProductsHandler(db)
        self._update_handler  = UpdateProductPriceHandler(db)

    def create_product(self, body: dict) -> dict:
        cmd = CreateProductCommand(**body)
        result = self._create_handler.handle(cmd)
        return result.__dict__

    def get_product(self, product_id: str) -> dict:
        result = self._get_handler.handle(GetProductQuery(product_id=product_id))
        return result.__dict__

    def list_products(self, min_price: float = None, in_stock_only: bool = False) -> dict:
        result = self._list_handler.handle(
            ListProductsQuery(min_price=min_price, in_stock_only=in_stock_only)
        )
        return {"items": [i.__dict__ for i in result.items], "total": result.total}

    def update_price(self, product_id: str, body: dict) -> dict:
        cmd = UpdateProductPriceCommand(product_id=product_id, **body)
        result = self._update_handler.handle(cmd)
        return result.__dict__


# ═══════════════════════════════════════════════════════════════════════════════
# COMPOSITION ROOT
# ═══════════════════════════════════════════════════════════════════════════════

if __name__ == "__main__":
    db  = InMemoryDatabase()
    api = ProductAPI(db)

    # Create
    created = api.create_product({"name": "Widget Pro", "description": "A great widget", "price": 29.99, "stock": 100})
    print(f"Created: {created['name']} (id={created['id'][:8]}...)")

    # Get
    fetched = api.get_product(created["id"])
    print(f"Fetched: {fetched['name']} @ £{fetched['price']}")

    # List
    listing = api.list_products(in_stock_only=True)
    print(f"Listed: {listing['total']} in-stock products")

    # Update price
    updated = api.update_price(created["id"], {"new_price": 24.99, "reason": "Promotional discount"})
    print(f"Price updated: £{updated['old_price']} → £{updated['new_price']}")

Production-Ready Example

In production, Vertical Slice Architecture scales elegantly to large teams and large codebases. The key production concerns reveal why organizations adopting this pattern rarely go back.

Key Production Concerns

  • Deliberate code duplication over coupling: When two slices need similar validation logic, resist the urge to extract a shared ValidationUtils class immediately. Accept the duplication. If the same logic appears in three or more slices and the business semantics are genuinely identical (not just incidentally similar), extract it to the Shared Kernel. Premature extraction creates cross-slice coupling that defeats the purpose of the architecture
  • The Shared Kernel discipline: Everything in the Shared Kernel is a shared liability — when it changes, every slice is potentially affected. Treat additions to the Shared Kernel with the scrutiny of a public API change. Keep it to: domain entities, value objects, database connection, cross-cutting middleware, and genuine utility functions
  • Feature flags per slice: Each slice can be independently feature-flagged. "Create Product v2" and "Create Product v1" can coexist as separate slices with a routing decision in the HTTP layer — no changes to other slices
  • Database access strategy per slice: Commands that write data may use a full ORM with domain objects. Read-heavy queries may bypass the domain model entirely and query the database directly for a flat DTO — maximum performance without architectural compromise. Each slice chooses independently

Production Folder Structure

src/
├── features/                          # All feature slices
│   ├── products/
│   │   ├── create-product/
│   │   │   ├── create_product.py      # Command + Handler + Response (single file)
│   │   │   └── test_create_product.py # Slice test — completely isolated
│   │   ├── get-product/
│   │   │   └── get_product.py
│   │   ├── list-products/
│   │   │   └── list_products.py
│   │   ├── update-product-price/
│   │   │   └── update_product_price.py
│   │   └── delete-product/
│   │       └── delete_product.py
│   ├── orders/
│   │   ├── place-order/
│   │   │   ├── place_order.py         # Complex slice — may use domain objects internally
│   │   │   └── test_place_order.py
│   │   ├── cancel-order/
│   │   │   └── cancel_order.py
│   │   └── get-order-history/
│   │       └── get_order_history.py   # Simple query slice — direct DB access
│   └── users/
│       ├── register-user/
│       └── update-profile/
│
├── shared/                            # Shared Kernel — minimal, curated
│   ├── domain/
│   │   ├── product.py                 # Domain entity
│   │   └── order.py
│   ├── infrastructure/
│   │   ├── database.py                # DB connection
│   │   └── event_bus.py
│   └── middleware/
│       ├── auth.py
│       └── error_handler.py
│
└── main.py                            # Composition root — register all slices

Slice Test Strategy

Each slice has its own test file that tests only that slice in complete isolation:

# features/products/create-product/test_create_product.py

def test_create_product_succeeds():
    """This test touches NO other slices, NO external services."""
    db = InMemoryDatabase()
    handler = CreateProductHandler(db)

    result = handler.handle(CreateProductCommand(
        name="Widget", description="A fine widget", price=9.99, stock=50
    ))

    assert result.name == "Widget"
    assert result.price == 9.99
    assert result.id is not None
    assert db.products[result.id].stock == 50

def test_create_product_rejects_negative_price():
    db = InMemoryDatabase()
    handler = CreateProductHandler(db)

    with pytest.raises(ValueError, match="non-negative"):
        handler.handle(CreateProductCommand(name="X", description="", price=-1.0, stock=0))

def test_create_product_rejects_blank_name():
    db = InMemoryDatabase()
    handler = CreateProductHandler(db)

    with pytest.raises(ValueError, match="required"):
        handler.handle(CreateProductCommand(name="   ", description="", price=9.99, stock=0))

Real-World Use Cases

Vertical Slice Architecture excels in scenarios where feature count is high, team size is large, or the rate of feature delivery is the primary concern.

1. High-Volume Feature-Rich Applications

E-commerce platforms with hundreds of distinct operations — browse product, apply coupon, calculate shipping, process return, update wishlist, track order — are a natural fit for Vertical Slice. Each operation is its own slice. Teams can own entire product domains (orders/, catalog/, shipping/) and work in complete isolation. A change to "Apply Coupon" has zero risk of breaking "Track Order" because they are separate slices sharing only the Shared Kernel.

2. API-First Microservice Candidates

When a monolith begins to show signs that certain capabilities should be extracted to microservices, a Vertical Slice organization makes the extraction trivial. Each slice in the monolith is already a candidate microservice — it has its own data access, its own business logic, its own request/response models. Extracting "Place Order" to a microservice means moving one slice directory to a new service, not untangling years of shared service class entanglement.

3. CQRS Applications

CQRS is Vertical Slice's natural partner. Commands (write operations) and Queries (read operations) are different slices with different optimizations. PlaceOrderCommand uses domain objects, transactional writes, and domain event publishing. GetOrderHistoryQuery bypasses the domain model entirely, queries the database with a single optimized SQL join, and returns a flat DTO — zero domain object instantiation, maximum performance. The same codebase supports both patterns simultaneously without conflict.

4. Large Teams with Feature Ownership

When multiple teams — Checkout Team, Search Team, Recommendations Team, Account Team — develop the same application, Vertical Slice provides natural team boundaries. Each team owns its slice directory. Code reviews stay within team boundaries. Merge conflicts are eliminated because teams never modify the same files. Feature ownership is explicit and enforced by folder structure.

5. Rapid Prototyping and MVP Development

Vertical Slice allows the simplest possible implementation for each feature during prototyping. "Get Product" can be a 10-line function that reads from a dictionary. When the prototype proves the concept, "Get Product" evolves to a real database query — without touching any other slice. The architecture scales from prototype to production without restructuring.

6. Flutter Mobile Applications

Flutter applications are feature-rich by nature — each screen or user action is a distinct feature. Vertical Slice organizes Flutter code by screen or user action rather than by widget type, bloc, or repository. The "Add to Cart" slice contains its BLoC, its API call, its UI event handling, and its model transformation all together. Understanding the "Add to Cart" feature means reading one directory, not navigating across blocs/, repositories/, models/, and widgets/ directories.

Language-Specific Mistakes and Anti-Patterns

Common Mistakes Across All Languages

  • Over-extracting to the Shared Kernel: Creating a ProductValidator in the Shared Kernel because two slices validate product names — if the validation rules differ even slightly, extraction forces all slices to accept the shared rule or add exceptions. Accept the duplication. Extract only when three or more slices share genuinely identical business semantics
  • Slice dependencies on other slices: PlaceOrderHandler importing GetProductHandler to check stock availability — slices must not depend on each other. If "Place Order" needs product data, it queries the database directly or uses a Shared Kernel domain entity. Cross-slice handler dependencies create hidden coupling that defeats the isolation promise
  • Fat Shared Kernel: The Shared Kernel grows to include OrderService, ProductService, UserService — effectively recreating a traditional service layer inside the Shared Kernel. The Shared Kernel is for domain entities, infrastructure, and genuine utilities only — never for business logic
  • One slice per HTTP method: Creating a GetProductHandler, PostProductHandler, and PatchProductHandler rather than GetProductHandler, CreateProductHandler, and UpdateProductPriceHandler. Slices are organized by business capability, not HTTP method. A slice name should describe what the user is doing, not which HTTP verb they used
  • No slice boundaries: Writing all feature code in one giant ProductController with 20 methods — this is a layered anti-pattern wearing a Vertical Slice label. Each discrete business capability must be its own slice with its own command/query, handler, and response types

Language-Specific Pitfalls

  • Python: Resist putting all handlers in views.py or services.py — these are layered patterns. Use features/create_product/handler.py, features/get_product/handler.py. Avoid Django's automatic model serialization that couples response shape to the database model — each slice defines its own response dataclass. Use pytest fixtures scoped to the slice, not shared across the entire test suite
  • TypeScript: Avoid importing types from one slice into another — if PlaceOrderHandler imports GetProductResponse from the get-product slice, the slices are coupled. Define separate DTOs per slice even if they look similar. Use NestJS modules with forFeature() to scope each slice's dependencies — avoid a single global AppModule that imports everything
  • Java: Avoid Spring's @Service beans that span multiple features — a ProductService bean shared by CreateProductHandler, GetProductHandler, and UpdateProductPriceHandler is a layered service layer, not Vertical Slices. Use package-private visibility for handler classes when possible — CreateProductHandler should not be callable from the UpdateProductPrice package
  • JavaScript: Without types, slice isolation is maintained by convention and linting. Use eslint-plugin-boundaries to fail the build when one feature directory imports from another. Each slice's test file should require no imports from other slice directories — if it does, the Shared Kernel needs a new entry or the slices are overcoupled
  • C#: The canonical C# implementation uses MediatR with IRequest<TResponse> and IRequestHandler<TRequest, TResponse> — each slice defines its own Request and Handler as nested classes within a static feature class (public static class CreateProduct). Avoid [ApiController] classes with more than one route method — each HTTP endpoint should be in its own minimal API MapXxx() call registered by the slice itself
  • PHP: Avoid Eloquent model methods that contain feature-specific business logic — Eloquent models are Shared Kernel entities. Each slice's handler should perform its own database query using the ORM's query builder rather than relying on methods defined on the model for another slice's needs. Use Laravel's Route::prefix() grouping per feature area
  • Go: Go's flat package structure makes cross-slice imports too easy — use sub-packages per feature (products/createproduct, products/getproduct) and keep handler structs unexported from outside their package. Use go/analysis tools or custom linters to verify that feature packages don't import each other. Avoid global var service instances accessible across packages
  • Rust: Use Rust modules per slice (mod create_product, mod get_product) with pub(crate) visibility for handler structs — this prevents cross-slice handler usage while allowing the composition root to wire everything. Use axum's Router::nest() to compose per-slice routers into the application router without coupling slices to each other
  • Dart: In Flutter, avoid shared BLoC or Cubit instances across features — each feature slice has its own state management. Use GetIt or Riverpod scoped providers per feature so that one feature's state doesn't bleed into another. Keep feature directories (lib/features/create_product/, lib/features/get_product/) with no cross-feature imports
  • Swift: In SwiftUI apps, avoid shared ObservableObject ViewModels that span multiple features — each feature screen has its own ViewModel. Use Swift's internal access control to prevent cross-feature handler access within a module. Each feature folder (CreateProduct/, GetProduct/) should be a self-contained unit testable with XCTest and no other feature's code
  • Kotlin: In Android apps, avoid shared ViewModel classes that manage multiple features — each feature screen has its own ViewModel. Use Hilt's @ViewModelScoped to scope each handler's dependencies to the feature's lifecycle. In Ktor, use Route.route("/feature") {} blocks per feature to co-locate routing and handling

Frequently Asked Questions

Is code duplication acceptable in Vertical Slice Architecture?

Yes — deliberately and explicitly. Vertical Slice Architecture values feature cohesion and isolation above DRY (Don't Repeat Yourself). If two slices have similar-looking validation logic, that similarity may be incidental — the rules might diverge as requirements evolve. Premature extraction to a shared utility creates coupling between slices that makes future changes harder. The guideline: tolerate duplication between slices unless three or more slices share semantically identical business logic that has zero chance of diverging. Even then, extract to the Shared Kernel cautiously.

How do I handle cross-cutting concerns like authentication and logging?

Cross-cutting concerns live in the Shared Kernel as middleware or interceptors that wrap the HTTP layer — not inside individual slices. Authentication middleware verifies the request before it reaches any slice handler. Request logging middleware records every request and response. Error handling middleware catches exceptions from any slice and maps them to HTTP responses. These concerns are genuinely cross-cutting — they apply identically to every slice — which is why they belong in the Shared Kernel rather than being duplicated per slice.

How does Vertical Slice relate to Domain-Driven Design?

They are compatible and complementary. DDD concepts — bounded contexts, aggregates, domain events, value objects — live in the Shared Kernel as foundational building blocks. Individual slices implement use cases (what DDD calls "application services") that operate on those domain objects. A complex "Place Order" slice may internally use DDD patterns: load the Order aggregate, invoke domain methods, collect domain events, commit to the event store. A simple "Get Product" slice may skip the domain model entirely and query a read model directly. DDD provides the domain modeling vocabulary; Vertical Slice provides the feature organization structure.

What is the right granularity for a slice?

A slice should represent one discrete, user-initiated business action. "Create Product" is one slice. "Update Product" is too broad — it should be "Update Product Price", "Update Product Stock", "Update Product Description" (three slices). "Get Product Details" and "Get Product Summary" might be separate slices if their response shapes and query optimization needs differ significantly. The test: can a developer understand exactly what this slice does by reading only its files? If the answer is yes, the granularity is right.

Should I use a Mediator library or dispatch handlers directly?

Both approaches work. A Mediator (MediatR in .NET, a custom dispatcher in other languages) adds a layer of indirection that decouples HTTP controllers from handler classes — the controller sends a CreateProductCommand to the Mediator, which finds and calls CreateProductHandler. This enables pipeline behaviors (validation, logging, caching) to be applied uniformly across all slices without modifying individual handlers. Direct dispatch (the HTTP layer instantiates or injects the handler directly) is simpler, more explicit, and perfectly adequate for smaller applications. The Mediator becomes valuable at scale when pipeline behaviors are needed.

How does Vertical Slice Architecture scale to hundreds of features?

It scales exceptionally well — better than layered architecture at large scale. With hundreds of features, a layered codebase has OrderController, OrderService, and OrderRepository files each containing hundreds of methods spread across multiple directories. Finding all the code for "Cancel Order" requires reading three large files. In Vertical Slice, "Cancel Order" is one directory. Navigating hundreds of features means navigating features/ subdirectories — each directory is a feature, each feature is self-contained. Large-scale Vertical Slice codebases are often described as uniquely easy to navigate: you find the feature by name, read the files in the feature directory, and you understand the complete behavior.

Key Takeaways

Language-Specific Best Practices

  • Python: Organize as src/features/<feature-name>/handler.py; use @dataclass for commands, queries, and responses within each slice; use pytest with no shared fixtures across slice tests; enforce slice boundaries with import-linter rules in CI
  • TypeScript: Use static nested namespaces or module files per feature (create-product.ts containing Command, Response, Handler as named exports); use NestJS feature modules or Express sub-routers per feature; enforce boundaries with eslint-plugin-boundaries; use strict TypeScript for all command/response types
  • Java: Use static nested classes within a feature class (public class CreateProduct { record Command; record Response; class Handler }) or separate package-private classes per feature package; use Spring Boot's auto-configuration with @Component and package-level scanning; use ArchUnit in CI to verify no cross-feature package imports
  • JavaScript: Use feature directories with index.js barrel exports; use ESLint import/no-restricted-paths to enforce no cross-feature imports; use Jest with describe blocks scoped to each feature; document command/response shapes with JSDoc
  • C#: Use MediatR with static nested classes in a feature class; register all handlers with AddMediatR(typeof(Program).Assembly); use Minimal API MapEndpoint() per feature class; use FluentValidation AbstractValidator<TCommand> per slice; use Vertical Slice templates from the CleanArchitecture or FastEndpoints NuGet packages
  • PHP: Organize as app/Features/CreateProduct/Handler.php; use Laravel route groups per feature area; use Form Request classes per slice for HTTP-layer validation; use PHPUnit with no shared test utilities between slice tests; enforce boundaries with phparkitect
  • Go: Use sub-packages per feature (internal/features/products/createproduct); export only the handler constructor function; use chi or gorilla/mux sub-routers mounted per feature; use go test ./... with table-driven tests scoped to each feature package
  • Rust: Use mod declarations per feature with pub(crate) handler visibility; use axum::Router::merge() to compose per-feature routers; use #[cfg(test)] modules within each feature file for co-located tests; use thiserror for per-slice error types
  • Dart: Organize Flutter features as lib/features/<feature>/; use Riverpod or BLoC scoped per feature; co-locate widget, state, and data access in the feature directory; use flutter_test with no shared test helpers across features
  • Swift: Organize as Sources/Features/<Feature>/; use internal access control for feature-specific types; use SwiftUI View and ViewModel co-located per feature; use XCTest with setUp() scoped to each feature test class; use Combine or async/await within slices without sharing Publishers across features
  • Kotlin: Organize as src/main/kotlin/features/<feature>/; use internal visibility for feature-specific classes; use Hilt with @ViewModelScoped per feature; use KoTest or JUnit5 with per-feature test classes; use Ktor's Route extension functions per feature for self-contained routing

📋 When to Use Vertical Slice Architecture:

  • The application has many distinct user-facing features (10+) with different complexity levels per feature
  • Multiple developers or teams work on the same codebase simultaneously — feature ownership needs to be explicit and conflict-free
  • Feature delivery speed is a priority — developers should be able to add a new feature by touching one place
  • The application is a candidate for eventual microservice extraction — each slice is a microservice-in-waiting
  • The codebase uses CQRS — commands and queries naturally map to separate slices with different optimization strategies
  • You are tired of the "which service does this belong in?" question — each feature owns its own service logic

⚠️ When NOT to Use Vertical Slice Architecture:

  • The application has very few features (under 5–10) — the slice organization adds structure overhead without proportionate benefit
  • The team is small (1–2 developers) and the codebase is simple — a well-organized layered structure is sufficient
  • The domain has very strong shared business rules that truly apply identically across many features — heavy domain logic belongs in the Shared Kernel and may indicate Hexagonal or Onion is a better fit
  • The codebase already has a well-functioning layered architecture and the team is productive — architectural refactoring has a cost; do it only when pain is real
  • All features are simple, uniform CRUD operations with identical validation and business logic — if all slices look identical, the slice boundaries add friction without benefit

🛠️ Best Practices Across Languages:

  1. Name slices by user intent, not HTTP methodCreateProduct, PlaceOrder, CancelSubscription not PostProduct, PostOrder, DeleteSubscription
  2. Tolerate duplication between slices — resist the urge to extract shared helpers at the first sign of similarity; wait for three identical usages with provably identical semantics
  3. Keep the Shared Kernel small and stable — every addition to the Shared Kernel affects every slice; treat it as a public API change requiring careful review
  4. One test file per slice — each slice's tests are self-contained, test only that slice, and require no other slice's code to run
  5. Handlers own their data access — each slice's handler performs its own database query in the way most appropriate for that slice (ORM, raw SQL, direct query) — no shared repository class
  6. Register slices at the composition root — the main.py, Program.cs, or main.go registers all slice handlers and routes; slices never self-register
  7. Each slice chooses its own complexity level — simple CRUD slices use 10 lines; complex business slices use domain objects, events, and transactions — the architecture imposes no uniformity requirement

💡 Common Pitfalls:

  • The shared service re-emerges: After a few months, ProductService reappears in the Shared Kernel with methods called by multiple slices — this is the layered pattern fighting its way back. Actively prevent it: business logic belongs in slices, not in the Shared Kernel
  • Slice bloat: A "Place Order" slice grows to 500 lines with 10 methods — it is now a service class in disguise. Split it: PlaceOrder, ValidateOrderPayment, ReserveOrderInventory, SendOrderConfirmation are separate slices with clear single responsibilities
  • Integration test fever: Tests that test multiple slices together "for realism" — these are integration tests masquerading as unit tests. Each slice should be testable in complete isolation; integration tests should be a small, separate suite
  • Missing Shared Kernel boundaries: Domain entities evolve into Shared Kernel service classes that contain business logic — the Shared Kernel becomes a layered service layer by another name
  • Vertical Slice + God Handler: A handler that does too much — validates, loads domain objects, applies business rules, sends emails, updates three database tables, and publishes events all in one method. For complex features, the handler can delegate to collaborators internal to the slice; the slice boundary is what matters, not the internal structure

🎁 Advanced Techniques:

  • Pipeline behaviors: Apply cross-cutting handler behaviors (validation, logging, caching, performance monitoring) as decorator layers that wrap every handler — MediatR's IPipelineBehavior, a Python decorator, or Go middleware applied to handlers. This avoids duplicating cross-cutting logic in every slice while preserving slice isolation
  • Slice-level feature flags: Route requests to CreateProductV2Handler or CreateProductV1Handler based on a feature flag — two slices coexist, the HTTP layer decides which runs. A/B testing, canary releases, and gradual rollouts become trivial
  • Read model slices: Queries that bypass the domain model entirely and read from a denormalized read model (a view, a materialized table, an Elasticsearch index) — GetOrderHistoryQuery reads from a pre-joined order_history_view rather than joining five tables at query time. The query slice is optimized for read performance independently of any write slice
  • Event-driven slices: A slice that is triggered by a domain event rather than an HTTP request — SendWelcomeEmailOnUserRegistered is a slice with an event handler instead of an HTTP handler. The slice's input is a domain event from the event bus; its output is an email sent. Event-driven and HTTP-driven slices coexist in the same features directory
  • Saga slices: Long-running business processes that span multiple steps and may take minutes or hours — ProcessRefundSaga coordinates InitiateRefund, NotifyPaymentGateway, UpdateOrderStatus, and SendRefundConfirmation slices through a saga coordinator. Each step is its own slice; the saga manages the workflow between them
  • Contract testing per slice: Each slice defines its own contract test that verifies the input/output contract without using real infrastructure. Pact or similar tools can generate and verify contracts between slices (when slices become microservices) from these tests — the same test that verified the monolith slice verifies the extracted microservice

Vertical Slice Architecture is the architecture of pragmatic delivery at scale. It does not promise a perfectly clean domain model or absolute infrastructure independence — it promises something more immediately valuable: that adding a new feature takes a fraction of the time it would in a layered codebase, that changing one feature carries zero risk of breaking another, and that a developer can understand any individual feature by reading one file or one directory. In a world where software delivery speed and developer productivity are the primary competitive advantages, those promises translate directly to business value.


This guide is part of the Design Patterns in Action series, which covers:

Each guide covers all 11 programming languages with production-ready examples, language-specific best practices, and comprehensive anti-pattern analysis.