Patrón Publish-Subscribe
Difundir eventos a multiples suscriptores independientes. Los publicadores envian mensajes a un topico sin saber que suscriptores existen, habilitando acoplamiento ligero entre productores y consumidores.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Descripción General
En una cola punto-a-punto, cada mensaje es procesado por exactamente un consumidor. Pero muchos sistemas necesitan multiples reacciones independientes al mismo evento. Cuando se realiza un pedido, el servicio de inventario actualiza stock, el servicio de notificaciones envia email de confirmacion, y el servicio de analitica registra el evento. El patron Publish-Subscribe difunde un mensaje a todos los suscriptores de un topico, para que cada suscriptor reciba su propia copia y la procese independientemente.
Cuándo Usar
-
For alternatives, see Event-Carried State Transfer Pattern.
-
Multiples servicios necesitan reaccionar al mismo evento independientemente
-
Quieres desacoplar al productor de los consumidores (el productor no sabe quien consume)
-
Nuevos consumidores pueden agregarse sin modificar al productor
-
Diferentes consumidores procesan el evento a diferentes velocidades o con diferente logica
Solución
Python (Redis Pub/Sub)
import redis
import json
import threading
r = redis.Redis(host="localhost", port=6379)
# Publicador
def publish_order(order_id, total):
event = {"type": "order.created", "order_id": order_id, "total": total}
r.publish("order.events", json.dumps(event))
print(f"Published: {event}")
# Suscriptor 1: Servicio de inventario
def inventory_subscriber():
pubsub = r.pubsub()
pubsub.subscribe("order.events")
for message in pubsub.listen():
if message["type"] == "message":
event = json.loads(message["data"])
if event["type"] == "order.created":
print(f"[Inventory] Updating stock for order {event['order_id']}")
# Suscriptor 2: Servicio de notificaciones
def notification_subscriber():
pubsub = r.pubsub()
pubsub.subscribe("order.events")
for message in pubsub.listen():
if message["type"] == "message":
event = json.loads(message["data"])
if event["type"] == "order.created":
print(f"[Notification] Sending confirmation for order {event['order_id']}")
# Iniciar suscriptores en threads
threading.Thread(target=inventory_subscriber, daemon=True).start()
threading.Thread(target=notification_subscriber, daemon=True).start()
# Publicar eventos
publish_order(1, 99.99)
publish_order(2, 49.99)
JavaScript (RabbitMQ — fanout exchange)
import amqp from "amqplib";
async function setup() {
const conn = await amqp.connect("amqp://localhost");
const channel = await conn.createChannel();
// Fanout exchange difunde a todas las colas enlazadas
await channel.assertExchange("order.events", "fanout", { durable: true });
// Crear colas para cada suscriptor
const { queue: inventoryQueue } = await channel.assertQueue("inventory", {
durable: true,
});
const { queue: notificationQueue } = await channel.assertQueue("notification", {
durable: true,
});
// Enlazar colas al fanout exchange
await channel.bindQueue(inventoryQueue, "order.events", "");
await channel.bindQueue(notificationQueue, "order.events", "");
// Publicador
async function publishOrder(orderId, total) {
const event = JSON.stringify({ type: "order.created", orderId, total });
channel.publish("order.events", "", Buffer.from(event), {
persistent: true,
});
console.log(`Published: order ${orderId}`);
}
// Suscriptor 1: Inventario
channel.consume(inventoryQueue, (msg) => {
if (!msg) return;
const event = JSON.parse(msg.content.toString());
console.log(`[Inventory] Updating stock for order ${event.orderId}`);
channel.ack(msg);
});
// Suscriptor 2: Notificaciones
channel.consume(notificationQueue, (msg) => {
if (!msg) return;
const event = JSON.parse(msg.content.toString());
console.log(`[Notification] Sending confirmation for order ${event.orderId}`);
channel.ack(msg);
});
// Publicar eventos
await publishOrder(1, 99.99);
await publishOrder(2, 49.99);
}
setup();
Java (Spring + Kafka topics)
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class OrderEventPublisher {
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper objectMapper = new ObjectMapper();
public OrderEventPublisher(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
// Publicador
public void publishOrder(Long orderId, double total) {
try {
String event = objectMapper.writeValueAsString(
new OrderEvent("order.created", orderId, total)
);
kafkaTemplate.send("order-events", String.valueOf(orderId), event);
System.out.println("Published: order " + orderId);
} catch (Exception e) {
throw new RuntimeException("Failed to publish", e);
}
}
// Suscriptor 1: Inventario
@KafkaListener(topics = "order-events", groupId = "inventory-service")
public void handleInventory(String message) {
OrderEvent event = parseEvent(message);
if ("order.created".equals(event.getType())) {
System.out.println("[Inventory] Updating stock for order " + event.getOrderId());
}
}
// Suscriptor 2: Notificaciones
@KafkaListener(topics = "order-events", groupId = "notification-service")
public void handleNotification(String message) {
OrderEvent event = parseEvent(message);
if ("order.created".equals(event.getType())) {
System.out.println("[Notification] Sending confirmation for order " + event.getOrderId());
}
}
private OrderEvent parseEvent(String message) {
try {
return objectMapper.readValue(message, OrderEvent.class);
} catch (Exception e) {
throw new RuntimeException("Failed to parse event", e);
}
}
}
Explicación
El publicador envia mensajes a un topico (o exchange) sin saber que suscriptores existen. El broker mantiene una lista de suscripciones para cada topico. Cuando llega un mensaje, el broker entrega una copia a cada suscriptor.
Fanout exchange (RabbitMQ): Difunde cada mensaje a todas las colas enlazadas al exchange. Cada cola obtiene su propia copia, y cada consumidor lee de su cola independientemente.
Kafka consumer groups: Kafka usa consumer groups para implementar pub/sub. Cada consumer group obtiene su propia copia de cada mensaje. Dentro de un grupo, las particiones se distribuyen entre consumidores para paralelismo.
Redis Pub/Sub: Fire-and-forget. Si un suscriptor no esta escuchando cuando se publica un mensaje, lo pierde. Usa Redis Streams en su lugar para durabilidad.
El beneficio clave es acoplamiento ligero: el publicador no sabe ni le importan los suscriptores. Agregar un nuevo suscriptor (ej. un servicio de analitica) no requiere cambios al publicador ni a otros suscriptores.
Variantes
| Variante | Mecanismo | Caso de Uso | Compromiso |
|---|---|---|---|
| Fanout Exchange | RabbitMQ fanout | Todos los suscriptores reciben todos los mensajes | Sin filtrado a nivel broker |
| Topic Exchange | RabbitMQ topic con routing keys | Suscriptores filtran por patron | Ruteo mas complejo |
| Kafka Consumer Groups | Kafka topics + groups | Durable, particionado, reejecutable | Mas infraestructura |
| SNS + SQS | AWS SNS fanout a SQS queues | Administrado, durable por suscriptor | Solo AWS, costo por mensaje |
| Redis Pub/Sub | Redis channels | Simple, baja latencia | Sin durabilidad, mensajes perdidos si offline |
Qué Funciona
- Usa colas durables para cada suscriptor para que los mensajes sobrevivan reinicios del broker
- Dale a cada suscriptor su propia cola (no compartida) para que procesen independientemente
- Usa consumer groups en Kafka para escalar dentro de un servicio suscriptor
- Incluye tipo de evento en el mensaje para que los suscriptores puedan filtrar eventos relevantes
- Haz suscriptores idempotentes — el mismo evento puede entregarse mas de una vez
- Monitorea el lag del suscriptor — un suscriptor lento puede acumular una cola grande
- Usa dead-letter queues por suscriptor para que un suscriptor fallido no afecte a otros
Errores Comunes
- Cola compartida para multiples suscriptores: Todos los suscriptores compiten por mensajes en lugar de cada uno obtener una copia. Cada suscriptor necesita su propia cola.
- Redis Pub/Sub para eventos criticos: Los mensajes se pierden si un suscriptor esta offline. Usa Redis Streams o un broker durable para eventos que no deben perderse.
- Suscriptores sincronos bloqueando al publicador: Si el publicador espera acknowledgments de suscriptores, los suscriptores lentos bloquean al publicador. Usa entrega asincrona.
- Sin filtrado por tipo de evento: Los suscriptores procesan cada evento incluso si solo les importa un tipo. Usa topic exchanges o filtra en el consumidor.
- Agregar suscriptores sin escalar el broker: Mas suscriptores significa mas copias de mensajes. Asegura que el broker pueda manejar el throughput aumentado.
- No manejar consumidores lentos: Un suscriptor que no puede mantener el ritmo acumula mensajes. Establece max depth de cola y alertas.
Preguntas Frecuentes
¿En qué se diferencia pub/sub de una cola de mensajes?
En una cola de mensajes, cada mensaje es consumido por exactamente un consumidor. En pub/sub, cada mensaje se entrega a todos los suscriptores. Las colas son para distribucion de trabajo; pub/sub es para difusion de eventos.
¿Debería usar Kafka o RabbitMQ para pub/sub?
Kafka si necesitas durabilidad, replay, alto throughput y procesamiento particionado. RabbitMQ si necesitas configuracion mas simple, menor latencia y ruteo flexible. Para soluciones cloud administradas, SNS+SQS (AWS) o Service Bus topics (Azure) son buenas opciones.
¿Los suscriptores pueden procesar eventos a diferentes velocidades?
Si. Cada suscriptor tiene su propia cola y procesa independientemente. Un suscriptor lento no afecta a otros. Monitorea la profundidad de cola por suscriptor para detectar lag.
¿Qué pasa si un suscriptor está caído?
Con colas durables, los mensajes se acumulan en la cola del suscriptor mientras esta offline. Cuando se reconecta, procesa el backlog. Con Redis Pub/Sub, los mensajes se pierden.
¿Cómo agrego un nuevo suscriptor?
Crea una nueva cola, enlazala al topico/exchange, y comienza a consumir. No se necesitan cambios al publicador ni a los suscriptores existentes. Este es el beneficio principal del patron.
Temas Avanzados
Escenario: Pub/Sub para Notificaciones de Pedidos
// Pub/Sub: desacoplar productor de consumidores
class EventBus {
private subscribers = new Map<string, Set<(data: unknown) => void>>();
subscribe(event: string, handler: (data: unknown) => void): () => void {
if (!this.subscribers.has(event)) this.subscribers.set(event, new Set());
this.subscribers.get(event)!.add(handler);
// Retornar funcion de unsubscribe
return () => this.subscribers.get(event)?.delete(handler);
}
publish(event: string, data: unknown): void {
this.subscribers.get(event)?.forEach(handler => {
try { handler(data); }
catch (err) { console.error(`[PUB/SUB] Handler error for ${event}:`, err); }
});
}
}
// Eventos del dominio
interface OrderCreated { orderId: string; customerEmail: string; total: number; }
interface OrderShipped { orderId: string; trackingNumber: string; }
interface OrderCancelled { orderId: string; reason: string; }
// Uso: multiples subscribers desacoplados
const bus = new EventBus();
// Subscriber 1: enviar email
bus.subscribe("order.created", (data: OrderCreated) => {
emailService.send(data.customerEmail, "Order confirmed", `Total: ${data.total}`);
});
// Subscriber 2: actualizar analytics
bus.subscribe("order.created", (data: OrderCreated) => {
analytics.track("order_created", { total: data.total });
});
// Subscriber 3: invalidar cache
bus.subscribe("order.created", () => {
cache.invalidate("orders:recent");
});
// Subscriber 4: notificar shipping
bus.subscribe("order.shipped", (data: OrderShipped) => {
smsService.send(data.orderId, `Shipped: ${data.trackingNumber}`);
});
// Publisher: el servicio de pedidos
async function createOrder(order: Order) {
await db.save(order);
bus.publish("order.created", { orderId: order.id, customerEmail: order.email, total: order.total });
}
// El publisher no conoce los subscribers
Lecciones:
- Pub/Sub desacopla productor de consumidores
- El publisher no conoce quienes son los subscribers
- Multiples subscribers reaccionan al mismo evento
- Anadir nuevo subscriber no requiere cambiar el publisher
- En sistemas distribuidos: usar Kafka, RabbitMQ, SNS+SQS
- En memoria: EventEmitter o EventBus custom
### Pub/Sub vs Observer: cual uso?
Pub/Sub es mas amplio: el publisher no conoce los subscribers, hay un bus/broker en medio. Observer es mas directo: el sujeto conoce a los observadores. Usa Pub/Sub para sistemas distribuidos o cuando quieres desacoplamiento total. Usa Observer para notificaciones dentro de un mismo modulo. Pub/Sub escala mejor: el broker enruta eventos. Observer es mas simple: no hay broker. Para microservicios, Pub/Sub. Para reactive forms, Observer. Recursos Relacionados
Message Queue Load Leveling Pattern
Smooth traffic spikes by placing a queue between a producer and a consumer. The producer writes messages at any rate; the consumer processes them at a steady pace.
PatternServerless Fanout Pattern
Broadcast a single event to multiple independent consumers via SNS, EventBridge, or SQS so each consumer processes the event without coupling.
PatternDead Letter Channel Pattern
Route unprocessable messages to a separate dead letter queue for inspection and replay. Prevent poison messages from blocking the main queue indefinitely.
PatternActor Model Pattern
Isolate state in actors that communicate only via messages. Each actor processes one message at a time, eliminating shared-state concurrency bugs by design.
PatternMessage Deduplication Pattern
Prevent duplicate processing by tracking message IDs with idempotency keys. Consumers check a store before processing to skip messages already handled.
PatternReactive Streams Pattern
Process asynchronous data streams with backpressure. Subscribers request N items at a time, preventing fast producers from overwhelming slow consumers.