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]
endArquitectura 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.tsSolució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.