Patrones de arquitectura de microservicios que realmente funcionan
Lecciones prácticas de diseñar y operar microservicios a escala: comunicación por eventos, límites de servicio y los patrones que sobrevivieron.

El golpe de realidad de los microservicios
Después de años construyendo y operando microservicios, he aprendido que lo más difícil no es dividir un monolito, sino saber dónde dividirlo y cómo lograr que las piezas se comuniquen entre sí de forma confiable.
Este artículo repasa los patrones que, en mi experiencia, han sobrevivido a producción.
Cómo encontrar los límites de un servicio
¿El error más común? Dividir por capa técnica en lugar de por dominio de negocio.
// ❌ Technical split — leads to tight coupling
// services/user-api/
// services/user-database/
// services/user-cache/
// ✅ Domain split — each service owns its full stack
// services/identity/ → auth, profiles, permissions
// services/catalog/ → products, categories, search
// services/orders/ → checkout, fulfillment, returnsEl Diseño Guiado por el Dominio (Domain-Driven Design) nos da el vocabulario para esto: los Contextos Delimitados (Bounded Contexts). Cada servicio debería corresponder a un contexto delimitado con propiedad clara sobre sus datos y su comportamiento.
Comunicación basada en eventos
Las llamadas HTTP síncronas entre servicios crean cadenas frágiles. Cuando el Servicio A llama al Servicio B, que a su vez llama al Servicio C, un fallo en cualquier punto rompe todo el flujo.
La comunicación basada en eventos desacopla los servicios:
// Order service — publishes event after checkout
interface OrderCreatedEvent {
type: "order.created";
data: {
orderId: string;
customerId: string;
items: Array<{ productId: string; quantity: number }>;
total: number;
};
metadata: {
timestamp: string;
correlationId: string;
};
}
async function createOrder(input: CreateOrderInput): Promise<Order> {
const order = await db.orders.create(input);
await eventBus.publish({
type: "order.created",
data: {
orderId: order.id,
customerId: order.customerId,
items: order.items,
total: order.total,
},
metadata: {
timestamp: new Date().toISOString(),
correlationId: generateId(),
},
});
return order;
}Los servicios de inventario, notificaciones y analítica se suscriben, cada uno por su cuenta, a order.created y reaccionan de forma independiente.
El patrón Saga para transacciones distribuidas
Cuando un proceso de negocio abarca varios servicios, no puedes usar una transacción de base de datos. Las sagas coordinan el flujo:
| Paso | Servicio | Acción | Compensación |
|---|---|---|---|
| 1 | Pedidos | Crear pedido | Cancelar pedido |
| 2 | Pago | Cobrar tarjeta | Reembolsar pago |
| 3 | Inventario | Reservar stock | Liberar stock |
| 4 | Envíos | Programar entrega | Cancelar envío |
Si el paso 3 falla, ejecutas las compensaciones en orden inverso: primero reembolsas el pago y luego cancelas el pedido. Se habla de coreografía cuando los servicios reaccionan a eventos, o de orquestación cuando un coordinador central gestiona el flujo.
El patrón API Gateway
Un API gateway se ubica entre los clientes y los servicios, y se encarga de:
- Enrutamiento — dirigir cada solicitud al servicio correcto
- Autenticación — validar los tokens una sola vez, no en cada servicio
- Limitación de tasa (rate limiting) — proteger los servicios de los picos de tráfico
- Agregación de respuestas — combinar datos provenientes de varios servicios
// Simplified gateway route example
async function getProductPage(productId: string) {
const [product, reviews, recommendations] = await Promise.all([
catalogService.getProduct(productId),
reviewService.getReviews(productId),
recommendationService.getSimilar(productId),
]);
return { product, reviews, recommendations };
}El patrón Circuit Breaker
Cuando un servicio corriente abajo empieza a fallar, si sigues llamándolo terminas propagando fallos en cascada por todo tu sistema. El patrón circuit breaker evita justamente eso:
class CircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: "closed" | "open" | "half-open" = "closed";
constructor(
private threshold: number = 5,
private resetTimeout: number = 30000,
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
if (Date.now() - this.lastFailure > this.resetTimeout) {
this.state = "half-open";
} else {
throw new Error("Circuit is open");
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = "closed";
}
private onFailure() {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = "open";
}
}
}La observabilidad no es opcional
En un sistema distribuido, vas a tener problemas que atraviesan varios servicios. Sin una observabilidad adecuada, depurarlos se vuelve imposible.
Los tres pilares:
- Logging estructurado — registros en JSON con IDs de correlación que permiten trazar una solicitud a través de todos los servicios
- Trazabilidad distribuida (distributed tracing) — herramientas como OpenTelemetry para visualizar el flujo de las solicitudes
- Métricas — métricas RED (Rate, Errors, Duration) por servicio
El ID de correlación es fundamental: propágalo en cada llamada entre servicios y en cada entrada de log para que puedas reconstruir el recorrido completo de una solicitud.
Puntos clave
- Dividir por dominio, no por capa — los contextos delimitados generan límites de servicio naturales
- Priorizar lo asíncrono — la comunicación basada en eventos evita los fallos en cascada
- Planificar para el fallo — circuit breakers, reintentos con backoff y sagas para las transacciones distribuidas
- Observarlo todo — no puedes arreglar lo que no puedes ver
- Empezar con un monolito — extrae servicios a medida que descubras dónde deben estar los límites
La mejor arquitectura es la más simple que cumple con tus requisitos. No adoptes microservicios solo porque estén de moda — hazlo cuando tu equipo y la complejidad de tu dominio realmente lo exijan.


