Saltar al contenido principal

Patrones Arquitectónicos Avanzados 🚀

Patrones para sistemas distribuidos y dominios complejos.


1. CQRS — Command Query Responsibility Segregation

Separa las operaciones de lectura (queries) de las operaciones de escritura (commands).

¿Qué es?

┌────────────────────────────────────────────────────────┐
│ APPLICATION │
├────────────────┬──────────────────────────────────────┤
│ WRITE SIDE │ READ SIDE │
│ (Commands) │ (Queries) │
├────────────────┼──────────────────────────────────────┤
│ Command │ Query │
│ Handler │ Handler │
│ │ │ │ │
│ ▼ │ ▼ │
│ ┌────────────┐ │ ┌────────────┐ │
│ │ Write DB │ │ │ Read DB │ │
│ │ (Normalized) (Denormalized) │
│ └────┬───────┘ │ └────────────┘ │
│ │ │ ▲ │
│ └─────────┼─────────┘ │
│ Event Stream (Sincronización) │
└────────────────┴──────────────────────────────────────┘

Ventajas

  • Modelos optimizados — Write DB normalizado, Read DB denormalizado
  • Escalabilidad independiente — Escalar lecturas y escrituras por separado
  • Performance — Read DB puede ser muy rápida (caché, índices, etc.)
  • Flexibilidad — Múltiples vistas de lectura del mismo dato

Implementación

// ✅ CQRS: Separar Commands y Queries

// WRITE SIDE
public class CreateOrderCommand
{
public Guid CustomerId { get; set; }
public List<OrderItemDto> Items { get; set; }
}

public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _repository;
private readonly IEventPublisher _eventPublisher;

public async Task<Guid> Handle(CreateOrderCommand cmd)
{
var order = Order.CreateNew(cmd.CustomerId, cmd.Items);
await _repository.SaveAsync(order);

// Publicar eventos → sincronizan con Read DB
foreach (var @event in order.GetDomainEvents())
{
await _eventPublisher.PublishAsync(@event);
}

return order.Id;
}
}

// Proyección: Sincronizar write DB → read DB
public class OrderCreatedProjection : IConsumer<OrderCreatedEvent>
{
private readonly IReadDbContext _readDb;

public async Task Handle(OrderCreatedEvent @event)
{
// Crear una vista desnormalizada en Read DB
var orderReadModel = new OrderReadModel
{
Id = @event.OrderId,
CustomerId = @event.CustomerId,
Total = @event.Total,
ItemCount = @event.Items.Count,
CreatedAt = DateTime.UtcNow
};

await _readDb.Orders.AddAsync(orderReadModel);
await _readDb.SaveChangesAsync();
}
}

// READ SIDE
public class GetOrdersQuery
{
public Guid CustomerId { get; set; }
}

public class GetOrdersQueryHandler : IQueryHandler<GetOrdersQuery, List<OrderReadModel>>
{
private readonly IReadDbContext _readDb;

public async Task<List<OrderReadModel>> Handle(GetOrdersQuery query)
{
// Lectura super rápida desde Read DB (desnormalizado)
return await _readDb.Orders
.Where(o => o.CustomerId == query.CustomerId)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync();
}
}

// Modelos diferentes optimizados para cada lado
public class Order // Write DB: Normalizado
{
public Guid Id { get; set; }
public Guid CustomerId { get; set; }
public List<OrderLineItem> LineItems { get; set; }
}

public class OrderReadModel // Read DB: Desnormalizado
{
public Guid Id { get; set; }
public Guid CustomerId { get; set; }
public decimal Total { get; set; }
public int ItemCount { get; set; }
public string CustomerName { get; set; }
public string CustomerEmail { get; set; }
public DateTime CreatedAt { get; set; }
// Agregados datos para lectura rápida
}

Cuándo usar

Usa CQRS cuando:

  • Patrones de lectura/escritura son muy diferentes
  • Necesitas escalabilidad asimétrica (muchas más lecturas que escrituras)
  • Sistema es complejo con múltiples vistas de los mismos datos

NO uses CQRS para:

  • Aplicaciones CRUD simples
  • Cuando patrones lectura/escritura son similares

2. Event Sourcing

En lugar de guardar estado actual, guarda todos los eventos que ocurrieron.

¿Qué es?

APROXIMACIÓN TRADICIONAL (State)
┌──────────┐ Put(Order) ┌──────────────┐
│ Order Id │ ─────────────> │ Current State│
│ 123 │ │ Status: Done │
└──────────┘ | Amount: 1000 │
└──────────────┘

EVENT SOURCING (Events)
┌──────────────────────────────────────┐
│ Event Stream for Order 123 │
├──────────────────────────────────────┤
│ 1. OrderCreated(123, cust_5, 1000) │
│ @2024-01-01 10:00 │
├──────────────────────────────────────┤
│ 2. OrderConfirmed(123) │
│ @2024-01-01 10:05 │
├──────────────────────────────────────┤
│ 3. PaymentProcessed(123, paid) │
│ @2024-01-01 10:06 │
├──────────────────────────────────────┤
│ 4. OrderShipped(123, track_abc) │
│ @2024-01-01 11:00 │
└──────────────────────────────────────┘
Reproducir: Order actual = Reproducir todos los eventos

Ventajas

  • Audit trail completo — Saber exactamente qué pasó y cuándo
  • Debugging — Reproducir estado en cualquier punto del tiempo
  • Replicación — Crear múltiples vistas publicando eventos
  • Temporal queries — "¿Cuál era el estado el 2024-01-01?"

Implementación

// Eventos de dominio
public abstract record DomainEvent
{
public Guid Id { get; } = Guid.NewGuid();
public long Version { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}

public record OrderCreatedEvent(
Guid OrderId,
Guid CustomerId,
decimal Total,
List<OrderItemDto> Items
) : DomainEvent;

public record OrderConfirmedEvent(
Guid OrderId,
DateTime ConfirmedAt
) : DomainEvent;

public record OrderShippedEvent(
Guid OrderId,
string TrackingNumber
) : DomainEvent;

// Almacenamiento de eventos
public interface IEventStore
{
Task AppendEventsAsync(Guid aggregateId, List<DomainEvent> events);
Task<List<DomainEvent>> GetEventsAsync(Guid aggregateId);
Task<List<DomainEvent>> GetEventsSinceAsync(Guid aggregateId, DateTime since);
}

// Agregado que maneja eventos
public class Order
{
private List<DomainEvent> _uncommittedEvents = new();

public Guid Id { get; private set; }
public Guid CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public decimal Total { get; private set; }
public List<OrderLineItem> LineItems { get; private set; } = new();

// Factory: Crear orden desde evento inicial
public static Order CreateFromEvents(List<DomainEvent> events)
{
var order = new Order();
foreach (var @event in events)
{
order.Apply(@event);
}
return order;
}

// Crear nueva orden
public static Order Create(CreateOrderDto dto)
{
var order = new Order();
var @event = new OrderCreatedEvent(
Guid.NewGuid(),
dto.CustomerId,
dto.Items.Sum(i => i.Price * i.Quantity),
dto.Items
);
order.Apply(@event);
order._uncommittedEvents.Add(@event);
return order;
}

// Persistencia: Confirmar orden
public void Confirm()
{
if (Status != OrderStatus.Pending)
throw new InvalidOperationException("Only pending orders can be confirmed");

var @event = new OrderConfirmedEvent(Id, DateTime.UtcNow);
Apply(@event);
_uncommittedEvents.Add(@event);
}

// Persistencia: Enviar orden
public void Ship(string trackingNumber)
{
if (Status != OrderStatus.Confirmed)
throw new InvalidOperationException("Only confirmed orders can be shipped");

var @event = new OrderShippedEvent(Id, trackingNumber);
Apply(@event);
_uncommittedEvents.Add(@event);
}

// Reproducir evento para reconstruir estado
private void Apply(DomainEvent @event)
{
switch (@event)
{
case OrderCreatedEvent oce:
Id = oce.OrderId;
CustomerId = oce.CustomerId;
Total = oce.Total;
Status = OrderStatus.Pending;
LineItems = oce.Items.Select(i =>
new OrderLineItem { ProductId = i.ProductId, Qty = i.Quantity }
).ToList();
break;

case OrderConfirmedEvent oce:
Status = OrderStatus.Confirmed;
break;

case OrderShippedEvent ose:
Status = OrderStatus.Shipped;
break;
}
}

public IReadOnlyList<DomainEvent> GetUncommittedEvents() => _uncommittedEvents.AsReadOnly();
public void ClearUncommittedEvents() => _uncommittedEvents.Clear();
}

// Uso
public class OrderService
{
private readonly IEventStore _eventStore;

public async Task<Guid> CreateOrderAsync(CreateOrderDto dto)
{
var order = Order.Create(dto);
await _eventStore.AppendEventsAsync(
order.Id,
order.GetUncommittedEvents().ToList()
);
order.ClearUncommittedEvents();
return order.Id;
}

public async Task ConfirmOrderAsync(Guid orderId)
{
var events = await _eventStore.GetEventsAsync(orderId);
var order = Order.CreateFromEvents(events);

order.Confirm();

await _eventStore.AppendEventsAsync(
order.Id,
order.GetUncommittedEvents().ToList()
);
}

// Temporal query: ¿Cuál era el estado el 2024-01-01 10:05?
public async Task<Order> GetOrderAtTimeAsync(Guid orderId, DateTime atTime)
{
var events = await _eventStore.GetEventsSinceAsync(orderId, atTime);
var order = Order.CreateFromEvents(events);
return order;
}
}

Cuándo usar

Usa Event Sourcing cuando:

  • Necesitas audit trail completo
  • Requieres temporal queries (estado en punto X del tiempo)
  • Trabajas con dominios financieros/críticos
  • Necesitas debugging avanzado

NO uses para:

  • Aplicaciones simples CRUD
  • Cuando consistencia eventual no es aceptable
  • Alto volumen de eventos pequeños

3. Saga Pattern

Transacciones distribuidas en microservicios.

PROBLEMA: Transacción en 3 servicios
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ Orders │ │ Inventory │ │ Payments │
│ Service │ │ Service │ │ Service │
└─────────────┘ └──────────────┘ └─────────────┘
│ │ │
└────────────────┴──────────────────┘
¿Cómo mantener consistencia?

SOLUCIÓN: Saga (Orquestación de transacciones)
┌──────────────────────────────────────────────┐
│ SAGA ORCHESTRATOR │
│ (Coordinador de transacción distribuida) │
├──────────────────────────────────────────────┤
│ 1. OrderService: CreateOrder ✓ │
│ 2. InventoryService: ReserveStock ✓ │
│ 3. PaymentService: ProcessPayment ✓ │
│ └─ Si falla: │
│ ROLLBACK (ejecutar compensaciones) │
│ - InventoryService: UndoReservation │
│ - OrderService: CancelOrder │
└──────────────────────────────────────────────┘

Orquestación (Orchestration Pattern)

Un orquestador central dirige los pasos:

public class CreateOrderSaga : IEventConsumer<CreateOrderCommand>
{
private readonly IOrderService _orderService;
private readonly IInventoryService _inventoryService;
private readonly IPaymentService _paymentService;
private readonly IEventPublisher _eventPublisher;

public async Task Handle(CreateOrderCommand cmd)
{
var sagaId = Guid.NewGuid();

try
{
// Paso 1: Crear orden
var order = await _orderService.CreateAsync(cmd);

// Paso 2: Reservar inventario
var inventoryReserved = await _inventoryService.ReserveAsync(
order.Id, cmd.Items
);

if (!inventoryReserved)
{
await _orderService.CancelAsync(order.Id);
throw new Exception("Could not reserve inventory");
}

// Paso 3: Procesar pago
var paymentResult = await _paymentService.ProcessAsync(
order.Id, order.Total
);

if (!paymentResult.Success)
{
// COMPENSACIÓN: Deshacer pasos anteriores
await _inventoryService.UndoReservationAsync(order.Id);
await _orderService.CancelAsync(order.Id);
throw new Exception("Payment failed");
}

// Éxito: Publicar evento
await _eventPublisher.PublishAsync(
new OrderCompletedEvent(order.Id)
);
}
catch (Exception ex)
{
// Log y manejo de error
await _eventPublisher.PublishAsync(
new OrderFailedEvent(sagaId, ex.Message)
);
}
}
}

Coreografía (Choreography Pattern)

Los servicios reaccionan a eventos (desacoplado):

// OrderService emite evento
public class OrderCreatedEventPublisher
{
private readonly IEventBus _eventBus;

public async Task PublishAsync(Order order)
{
await _eventBus.PublishAsync(
new OrderCreatedEvent(order.Id, order.Items)
);
}
}

// InventoryService escucha y reacciona
public class ReserveStockOnOrderCreated : IEventConsumer<OrderCreatedEvent>
{
private readonly IInventoryService _inventory;
private readonly IEventBus _eventBus;

public async Task Handle(OrderCreatedEvent @event)
{
try
{
await _inventory.ReserveAsync(@event.OrderId, @event.Items);

// Emitir éxito
await _eventBus.PublishAsync(
new StockReservedEvent(@event.OrderId)
);
}
catch
{
// Emitir fallo
await _eventBus.PublishAsync(
new StockReservationFailedEvent(@event.OrderId)
);
}
}
}

// PaymentService escucha StockReserved
public class ProcessPaymentOnStockReserved : IEventConsumer<StockReservedEvent>
{
private readonly IPaymentService _payment;
private readonly IEventBus _eventBus;

public async Task Handle(StockReservedEvent @event)
{
try
{
await _payment.ProcessAsync(@event.OrderId);
await _eventBus.PublishAsync(
new PaymentProcessedEvent(@event.OrderId)
);
}
catch
{
// Si pago falla, otros servicios escuchan y hacen rollback
await _eventBus.PublishAsync(
new PaymentFailedEvent(@event.OrderId)
);
}
}
}

// Si algo falla, una saga de COMPENSACIÓN se ejecuta
public class CompensateSagaOnPaymentFailure
: IEventConsumer<PaymentFailedEvent>
{
private readonly IInventoryService _inventory;
private readonly IOrderService _orders;

public async Task Handle(PaymentFailedEvent @event)
{
// Compensación:
await _inventory.UndoReservationAsync(@event.OrderId);
await _orders.CancelAsync(@event.OrderId);
}
}

4. Strangler Fig Pattern

Reemplazar monolito con microservicios sin stop.

FASE 1: Monolito con Facade
┌─────────────────────────────────┐
│ External Requests │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ Strangler Facade │ │
│ │ (Nuevo proxy) │ │
│ └────────────────────────────┘ │
│ │ │ │
│ ▼ (new) (legacy)▼ │
│ ┌────────────────────────────┐ │
│ │ New Microservice │ Monolith │ │
│ │ Orders Service │ Old Code │ │
│ └────────────────────────────┘ │
└─────────────────────────────────┘

FASE 2: Migración gradual
New Micro: 30% Monolith: 70%
New Micro: 60% Monolith: 40%
New Micro: 100% Monolith: 0% ← Desconectado

VENTAJA: Despliegue sin downtime

5. API Gateway Pattern

Una única entrada para múltiples servicios.

┌──────────────────────────────────┐
│ Clients │
│ (Web, Mobile, Third-party) │
└──────────────┬───────────────────┘

┌──────▼──────┐
│ API Gateway│
│ (Kong, Tyk) │
└─┬─┬─┬─┬────┬┘
│ │ │ │ │
┌─────┘ │ │ │ │
│ ┌──────┘ │ │ │
│ │ ┌──────┘ │ │
│ │ │ ┌──────┘ │
▼ ▼ ▼ ▼ ▼
┌────┐ ┌────┐ ┌────┐
│Orders│ │Inventory│ │Payments│
└────┘ └────┘ └────┘

Responsabilidades del API Gateway:
- Enrutamiento
- Autenticación/Autorización
- Rate limiting
- Transformación de payloads
- Logging/Tracing
- Cache

Comparativa de Patrones

PatrónCaso de UsoComplejidadCuándo Usarlo
CQRSLectura/Escritura asimétricaMediaMúltiples vistas, escalabilidad
Event SourcingAudit completoAltaDominio financiero, temporal queries
SagaTransacción distribuidaAlta3+ servicios, consistencia eventual
Strangler FigMonolito → MicroserviciosMediaMigración gradual
API GatewayGestionar múltiples serviciosBajaSiempre en microservicios

Resumen

┌─────────────────────────────────────────┐
│ PATRONES ARQUITECTÓNICOS AVANZADOS │
├─────────────────────────────────────────┤
│ CQRS → Escalabilidad asimétrica │
│ Event → Histórico completo, │
│ Sourcing debugging avanzado │
│ Saga → Transacciones distribuidas
│ Strangler → Migración sin downtime │
│ API Gateway → Punto único de entrada │
└─────────────────────────────────────────┘

Importante: No uses todos simultáneamente. Cada uno resuelve un problema específico.