Saltar al contenidoJMRG
Todas las entradas

Vertical Slice Architecture: Organización por Funcionalidad

architecturevertical-sliceclean-architecture

¿Qué es Vertical Slice Architecture?

En lugar de organizar el código por capas técnicas (Controllers, Services, Repositories), Vertical Slice Architecture organiza por funcionalidad o feature. Cada "slice" contiene todo lo necesario para una operación específica.

flowchart TB
    subgraph Traditional["Arquitectura por Capas"]
        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

Arquitectura por Capas vs Vertical Slice

Problema con Capas Tradicionales

src/
├── controllers/
│   ├── OrderController.ts      # Muchos métodos mezclados
│   ├── ProductController.ts
│   └── UserController.ts
├── services/
│   ├── OrderService.ts         # Dependencias cruzadas
│   ├── ProductService.ts
│   └── UserService.ts
├── repositories/
│   ├── OrderRepository.ts
│   ├── ProductRepository.ts
│   └── UserRepository.ts
└── models/
    ├── Order.ts
    ├── Product.ts
    └── User.ts

Solución: 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/

Implementación Práctica

1. Definir el 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 con toda la lógica

// 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. Validación
  const validation = validateOrder(command);
  if (!validation.success) {
    throw new ValidationError(validation.errors);
  }

  // 2. Verificar inventario
  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. Calcular 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. Crear orden en transacción
  const order = await prisma.$transaction(async (tx) => {
    // Crear orden
    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
          }))
        }
      }
    });

    // Actualizar inventario
    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 que usa el 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;
  }
}

Con 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 para despachar 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);
  }
}

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

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

Comparación

Aspecto Capas Vertical Slice
Cohesión Baja (por técnica) Alta (por feature)
Acoplamiento Alto entre capas Bajo entre slices
Cambios Tocan múltiples capas Tocan un slice
Testing Tests dispersos Tests focalizados
Onboarding Difícil de seguir Fácil de entender

Cuándo Usar

Situación Recomendación
Features independientes ✅ Ideal
Equipos por feature ✅ Muy útil
CRUD simple ❌ Overkill
Mucha lógica compartida ⚠️ Evaluar

Conclusión

Vertical Slice Architecture alinea la estructura del código con el dominio del negocio, haciendo más fácil entender, modificar y escalar cada funcionalidad de forma independiente.