Skip to contentJMRG
All posts

Vertical Slice Architecture: Organizing by Functionality

architecturevertical-sliceclean-architecture

What is Vertical Slice Architecture?

Instead of organizing code by technical layers (Controllers, Services, Repositories), Vertical Slice Architecture organizes by functionality or feature. Each "slice" contains everything needed for a specific operation.

flowchart TB
    subgraph Traditional["Layered Architecture"]
        direction TB
        C[Controllers Layer]
        S[Services Layer]
        R[Repositories Layer]
        C --> S --> R
    end

    subgraph Vertical["Vertical Slice"]
        direction TB
        F1[Feature: CreateOrder]
        F2[Feature: GetOrder]
        F3[Feature: CancelOrder]
    end

Layered Architecture vs Vertical Slice

Problem with Traditional Layers

src/
├── controllers/
│   ├── OrderController.ts      # Many mixed methods
│   ├── ProductController.ts
│   └── UserController.ts
├── services/
│   ├── OrderService.ts         # Cross dependencies
│   ├── ProductService.ts
│   └── UserService.ts
├── repositories/
│   ├── OrderRepository.ts
│   ├── ProductRepository.ts
│   └── UserRepository.ts
└── models/
    ├── Order.ts
    ├── Product.ts
    └── User.ts

Solution: Vertical Slices

src/
├── features/
│   ├── orders/
│   │   ├── create-order/
│   │   │   ├── CreateOrderCommand.ts
│   │   │   ├── CreateOrderHandler.ts
│   │   │   └── CreateOrderValidator.ts
│   │   ├── get-order/
│   │   │   ├── GetOrderQuery.ts
│   │   │   └── GetOrderHandler.ts
│   │   └── cancel-order/
│   │       ├── CancelOrderCommand.ts
│   │       └── CancelOrderHandler.ts
│   └── products/
│       ├── create-product/
│       └── list-products/
└── shared/
    ├── database/
    └── middleware/

Practical Implementation

1. Define the Slice

// features/orders/create-order/CreateOrderCommand.ts
export interface CreateOrderCommand {
  customerId: string;
  items: Array<{
    productId: string;
    quantity: number;
  }>;
  shippingAddress: Address;
}

export interface CreateOrderResult {
  orderId: string;
  total: number;
  estimatedDelivery: Date;
}

2. Handler with All Logic

// features/orders/create-order/CreateOrderHandler.ts
import { prisma } from '@/shared/database';
import { CreateOrderCommand, CreateOrderResult } from './CreateOrderCommand';
import { validateOrder } from './CreateOrderValidator';

export async function handleCreateOrder(
  command: CreateOrderCommand
): Promise<CreateOrderResult> {
  // 1. Validation
  const validation = validateOrder(command);
  if (!validation.success) {
    throw new ValidationError(validation.errors);
  }

  // 2. Check inventory
  const products = await prisma.product.findMany({
    where: {
      id: { in: command.items.map(i => i.productId) }
    }
  });

  for (const item of command.items) {
    const product = products.find(p => p.id === item.productId);
    if (!product || product.stock < item.quantity) {
      throw new InsufficientStockError(item.productId);
    }
  }

  // 3. Calculate total
  const total = command.items.reduce((sum, item) => {
    const product = products.find(p => p.id === item.productId)!;
    return sum + (product.price * item.quantity);
  }, 0);

  // 4. Create order in transaction
  const order = await prisma.$transaction(async (tx) => {
    // Create order
    const order = await tx.order.create({
      data: {
        customerId: command.customerId,
        total,
        status: 'PENDING',
        items: {
          create: command.items.map(item => ({
            productId: item.productId,
            quantity: item.quantity,
            price: products.find(p => p.id === item.productId)!.price
          }))
        }
      }
    });

    // Update inventory
    for (const item of command.items) {
      await tx.product.update({
        where: { id: item.productId },
        data: { stock: { decrement: item.quantity } }
      });
    }

    return order;
  });

  return {
    orderId: order.id,
    total: order.total,
    estimatedDelivery: calculateDeliveryDate(command.shippingAddress)
  };
}

3. Endpoint Using the Handler

// app/api/orders/route.ts (Next.js App Router)
import { handleCreateOrder } from '@/features/orders/create-order/CreateOrderHandler';

export async function POST(request: Request) {
  const command = await request.json();

  try {
    const result = await handleCreateOrder(command);
    return Response.json(result, { status: 201 });
  } catch (error) {
    if (error instanceof ValidationError) {
      return Response.json({ errors: error.errors }, { status: 400 });
    }
    throw error;
  }
}

With CQRS (Command Query Responsibility Segregation)

flowchart LR
    subgraph Commands
        C1[CreateOrder]
        C2[CancelOrder]
        C3[UpdateOrder]
    end

    subgraph Queries
        Q1[GetOrder]
        Q2[ListOrders]
        Q3[GetOrderHistory]
    end

    Commands --> WriteDB[(Write DB)]
    Queries --> ReadDB[(Read DB)]
// Mediator to dispatch commands/queries
class Mediator {
  private handlers = new Map<string, Handler>();

  register<T, R>(type: string, handler: Handler<T, R>): void {
    this.handlers.set(type, handler);
  }

  async send<T, R>(request: T & { type: string }): Promise<R> {
    const handler = this.handlers.get(request.type);
    if (!handler) throw new Error(`No handler for ${request.type}`);
    return handler.handle(request);
  }
}

// Usage
const mediator = new Mediator();
mediator.register('CreateOrder', new CreateOrderHandler());
mediator.register('GetOrder', new GetOrderHandler());

// In endpoint
const result = await mediator.send({
  type: 'CreateOrder',
  customerId: '123',
  items: [...]
});

Comparison

Aspect Layers Vertical Slice
Cohesion Low (by tech) High (by feature)
Coupling High between layers Low between slices
Changes Touch multiple layers Touch one slice
Testing Dispersed tests Focused tests
Onboarding Hard to follow Easy to understand

When to Use

Situation Recommendation
Independent features ✅ Ideal
Feature teams ✅ Very useful
Simple CRUD ❌ Overkill
Much shared logic ⚠️ Evaluate

Conclusion

Vertical Slice Architecture aligns code structure with the business domain, making it easier to understand, modify, and scale each functionality independently.