Message Dispatcher¶
1. Nombre del Patrón¶
- Nombre oficial: Message Dispatcher
- Categoría: Messaging Endpoints (Endpoints de Mensajería)
- Traducción contextual: Despachador de Mensajes
2. Resumen Ejecutivo¶
Message Dispatcher es un patrón de endpoint que centraliza la recepción de mensajes en un único consumidor y los distribuye internamente a handlers especializados según el tipo o contenido del mensaje. En lugar de que cada handler se conecte directamente al sistema de mensajería, un dispatcher actúa como punto único de entrada que inspecciona cada mensaje entrante y lo delega al handler apropiado dentro del mismo proceso.
El problema que resuelve es la organización interna del consumo de mensajes: cuando una aplicación debe procesar múltiples tipos de mensajes que llegan por un mismo canal, ¿cómo se estructura el código para que cada tipo de mensaje sea manejado por lógica especializada sin que todos los handlers necesiten conocer la infraestructura de messaging? Message Dispatcher responde a esta pregunta introduciendo un componente intermediario que desacopla la recepción del mensaje de su procesamiento, permitiendo que los handlers se escriban como lógica pura de negocio sin dependencias de messaging.
Este patrón es especialmente relevante en sistemas donde llegan eventos heterogéneos por un solo canal — como un stream de CDRs (Call Detail Records) en telecomunicaciones que debe despacharse a handlers de tarificación, auditoría y facturación — y donde se necesita una arquitectura interna limpia, testeable y extensible para distribuir esos mensajes.
3. Definición Detallada¶
Propósito¶
Message Dispatcher establece un único punto de consumo que recibe mensajes del sistema de mensajería y los despacha a performers (handlers) especializados dentro de la misma aplicación. Su propósito es separar la responsabilidad de recibir mensajes de la responsabilidad de procesarlos, creando una capa intermedia que gestiona la distribución interna.
Lógica Arquitectónica¶
En una aplicación que consume mensajes de un canal, la solución más directa es que cada handler se conecte independientemente al canal y filtre los mensajes relevantes. Pero esto tiene problemas: múltiples conexiones al broker, filtrado redundante, y acoplamiento directo de cada handler con la infraestructura de messaging.
Message Dispatcher invierte esta estructura. Un solo componente mantiene la conexión con el broker, consume todos los mensajes y los distribuye internamente. Los handlers se registran con el dispatcher indicando qué tipos de mensajes pueden procesar. Cuando llega un mensaje, el dispatcher:
- Inspecciona el tipo o contenido del mensaje.
- Busca el handler registrado para ese tipo.
- Invoca el handler pasándole el mensaje.
Esta arquitectura tiene consecuencias importantes:
- Desacoplamiento de infraestructura: los handlers no necesitan saber que los mensajes vienen de Kafka, RabbitMQ o cualquier otro broker. Solo reciben objetos de dominio.
- Punto único de gestión: el dispatcher centraliza cross-cutting concerns como logging, métricas, error handling y serialización.
- Extensibilidad: añadir un nuevo tipo de mensaje solo requiere registrar un nuevo handler, sin modificar la lógica de consumo ni los handlers existentes.
Principio de Diseño Subyacente¶
El principio es Single Point of Entry con distribución interna basada en tipo. Es la aplicación del principio de indirección dentro de los límites de un proceso: en lugar de que N handlers conozcan la infraestructura de messaging, un solo componente la conoce y distribuye a N handlers que solo conocen su dominio.
Problema Estructural que Resuelve¶
Cuando una aplicación debe manejar M tipos de mensajes, sin dispatcher cada handler necesita: conexión al broker, deserialización, filtrado, error handling, logging. Esto multiplica la complejidad por M y acopla cada pieza de lógica de negocio al framework de messaging. Message Dispatcher colapsa esta complejidad en un solo punto.
Contexto en el que Emerge¶
Message Dispatcher emerge cuando una aplicación recibe mensajes de tipos heterogéneos por uno o pocos canales y necesita distribuirlos a lógica especializada. Es especialmente común en monolitos modulares, servicios que consumen de topics con múltiples tipos de evento, y frameworks que necesitan enrutar mensajes a métodos anotados.
Por Qué No Es Trivial¶
Aunque un switch sobre el tipo de mensaje parece simple, las decisiones de diseño son más profundas:
- Registro dinámico vs. estático: ¿los handlers se registran en código (estático) o se descubren automáticamente (annotations, reflection)?
- Concurrencia: ¿el dispatch es síncrono o asíncrono? ¿Se puede despachar a múltiples handlers en paralelo?
- Error handling: ¿qué ocurre si un handler falla? ¿Se reintenta? ¿Se envía a dead-letter? ¿Afecta al procesamiento de otros mensajes?
- Ordenamiento: ¿el dispatcher garantiza el orden de procesamiento por tipo? ¿Por partición?
Relación con Sistemas Distribuidos y Mensajería¶
Message Dispatcher opera dentro de los límites de un proceso, no entre procesos. Es un patrón de organización interna del consumidor, no de distribución entre consumidores (eso es Competing Consumers o Content-Based Router). Sin embargo, su diseño impacta directamente la eficiencia del consumo: un dispatcher mal diseñado que bloquea en un handler lento retrasará todos los demás tipos de mensajes.
En la práctica, los frameworks modernos implementan Message Dispatcher de forma transparente:
- En Spring Integration, el
DispatchingChanneldespacha mensajes a handlers basándose en filtros. - En Spring Kafka,
@KafkaListenercon@KafkaHandleren una clase multi-método es un dispatcher implícito. - En MassTransit, los consumers se registran por tipo de mensaje y el framework despacha automáticamente.
- En NServiceBus,
IHandleMessages<T>registra handlers por tipo que el framework invoca.
4. Problema que Resuelve¶
El Problema Antes del Patrón¶
Sin Message Dispatcher, una aplicación que consume mensajes heterogéneos de un canal enfrenta:
- Acoplamiento handler-infraestructura: cada handler necesita conocer cómo conectarse al broker, cómo deserializar mensajes, cómo confirmar el procesamiento. La lógica de negocio queda mezclada con la lógica de mensajería.
- Filtrado redundante: si múltiples handlers consumen del mismo canal, cada uno debe implementar su propia lógica de filtrado para ignorar mensajes que no le corresponden.
- Cross-cutting duplicado: logging, métricas, error handling y tracing se repiten en cada handler.
- Testabilidad degradada: los handlers acoplados a la infraestructura de messaging son difíciles de testear unitariamente.
Síntomas del Problema¶
- Código de handlers que contiene más lógica de infraestructura que lógica de negocio.
- Un bloque
switch/casegigante en el consumidor principal que crece con cada tipo de mensaje. - Múltiples conexiones al broker desde el mismo proceso cuando una sola bastaría.
- Tests de handlers que requieren levantar un broker embebido o mock complejo del SDK de messaging.
- Dificultad para añadir un nuevo tipo de mensaje sin modificar código existente (violación de Open/Closed).
Impacto Operativo y Arquitectónico¶
Sin un dispatcher:
- La complejidad ciclomática del consumidor crece linealmente con cada tipo de mensaje, haciendo el código difícil de mantener y revisar.
- Los errores en el manejo de un tipo de mensaje pueden afectar a todos los demás si no hay aislamiento.
- El monitoreo por tipo de mensaje es difícil porque todo se procesa en el mismo bloque de código.
- Los desarrolladores evitan añadir nuevos tipos de mensaje porque cada adición implica tocar un componente ya complejo.
Riesgos Si No Se Implementa Correctamente¶
- Handler leak: handlers que no se desregistran producen memory leaks o procesamiento fantasma.
- Dispatcher bloqueante: un handler lento bloquea el dispatcher y retrasa todos los demás tipos de mensajes.
- Pérdida de contexto: si el dispatcher no propaga correlation IDs o trace context a los handlers, se pierde la trazabilidad.
- Fallback ausente: mensajes de tipo desconocido que no tienen handler registrado se pierden silenciosamente.
Ejemplos Reales¶
- Telecomunicaciones: un stream de CDR events llega a un servicio que debe despachar cada CDR al handler de tarificación, al handler de auditoría regulatoria y al handler de facturación. Sin dispatcher, cada handler filtra el mismo stream independientemente.
- E-commerce: un canal de eventos de orden (
OrderCreated,OrderPaid,OrderShipped,OrderCancelled) llega a un servicio de fulfillment que debe reaccionar de forma diferente a cada tipo. - IoT: un stream de telemetría con eventos de ubicación, temperatura y alerta de batería llega a un servicio que despacha cada tipo a un procesador especializado.
5. Contexto de Aplicación¶
Cuándo Usarlo¶
- Cuando una aplicación consume mensajes de tipos heterogéneos de uno o pocos canales.
- Cuando se quiere desacoplar los handlers de la infraestructura de messaging.
- Cuando se necesita centralizar cross-cutting concerns (logging, métricas, error handling) en un solo punto.
- Cuando se necesita una arquitectura extensible donde añadir un nuevo handler no requiera modificar código existente.
- Cuando los handlers deben ser testeables unitariamente sin dependencias de messaging.
Cuándo No Usarlo¶
- Cuando cada tipo de mensaje llega por un canal dedicado y cada canal tiene un solo consumidor. En ese caso, no hay necesidad de dispatch interno.
- Cuando el procesamiento es tan simple que un handler único basta y la indirección del dispatcher añade complejidad innecesaria.
- Cuando los handlers necesitan consumer groups separados o escalabilidad independiente — en ese caso, se necesitan consumidores independientes, no dispatch interno.
Precondiciones¶
- Existe un canal con mensajes de tipos heterogéneos.
- Los mensajes tienen un discriminador de tipo (header, campo tipo, tipo de clase tras deserialización).
- Los handlers son componentes dentro del mismo proceso que el dispatcher.
Restricciones¶
- El dispatcher y los handlers comparten el mismo ciclo de vida de proceso. Si un handler necesita escalar independientemente, Message Dispatcher no es el patrón adecuado.
- El throughput del dispatcher está limitado por el handler más lento (si el dispatch es síncrono y secuencial).
Dependencias¶
- Un sistema de mensajería del que se consumen mensajes.
- Un mecanismo de registro de handlers (manual, annotations, service discovery interno).
- Un discriminador en los mensajes que permita determinar el tipo.
Supuestos Arquitectónicos¶
- Los handlers son fast enough para no bloquear el dispatcher por períodos prolongados.
- El número de tipos de mensaje es conocido y acotado (o al menos gestionable con un handler de fallback).
- El framework o la aplicación gestiona el ciclo de vida de los handlers (creación, inyección de dependencias, destrucción).
Tipo de Sistemas Donde Aparece con Más Frecuencia¶
- Monolitos modulares que consumen de message brokers.
- Microservicios que consumen de topics con múltiples tipos de evento.
- Frameworks de messaging que proporcionan dispatch automático (Spring, MassTransit, NServiceBus).
6. Fuerzas Arquitectónicas¶
Acoplamiento vs. Flexibilidad¶
Message Dispatcher reduce el acoplamiento entre handlers y la infraestructura de messaging, pero introduce acoplamiento entre los handlers y el dispatcher. Si el API del dispatcher cambia, todos los handlers se ven afectados. El balance se logra manteniendo la interfaz del handler lo más simple posible (un solo método con el mensaje como parámetro).
Simplicidad vs. Robustez¶
Un dispatcher simple (lookup table de tipo → handler) es fácil de entender pero puede ser insuficiente para escenarios complejos (dispatch basado en contenido, dispatch a múltiples handlers, dispatch condicional). Un dispatcher robusto con todas esas capacidades es más poderoso pero más difícil de depurar.
Centralización vs. Autonomía¶
El dispatcher centraliza el control del flujo de mensajes, lo que facilita la observabilidad y el error handling global. Pero también introduce un single point of failure y un cuello de botella potencial. Si el dispatcher falla, todos los handlers dejan de recibir mensajes.
Rendimiento vs. Aislamiento¶
El dispatch síncrono dentro del thread del consumidor es simple y mantiene el orden, pero un handler lento bloquea todo. El dispatch asíncrono (thread pool, async/await) proporciona aislamiento pero complica el error handling y la gestión de backpressure.
Testabilidad vs. Overhead¶
Los handlers desacoplados de messaging son fáciles de testear unitariamente, lo cual es un beneficio significativo. Pero la capa adicional del dispatcher requiere sus propios tests de integración, añadiendo overhead al testing.
7. Estructura Conceptual del Patrón¶
Actores o Componentes Involucrados¶
- Message Channel: el canal del que llegan los mensajes.
- Dispatcher: el componente que consume mensajes del canal, inspecciona su tipo y los despacha al handler apropiado.
- Handler Registry: el mecanismo que mapea tipos de mensaje a handlers.
- Handlers (Performers): los componentes que contienen la lógica de negocio para procesar cada tipo de mensaje.
- Fallback Handler (opcional): handler que procesa mensajes cuyo tipo no tiene handler registrado.
Flujo Lógico¶
flowchart TD
A[(Canal)] -->|Consume mensaje| B[Dispatcher: Deserializa y determina tipo]
B -->|Consulta Handler Registry| C{Handler encontrado?}
C -->|Sí| D[Handler procesa el mensaje]
C -->|No| E[Fallback Handler / Dead-letter]
D --> F{Resultado}
F -->|Éxito| G[Acknowledge mensaje en el canal]
F -->|Error| H[Aplica política retry / dead-letter]
G --> I([Continúa con siguiente mensaje])
H --> I
E --> I
I --> A Responsabilidades¶
| Componente | Responsabilidad |
|---|---|
| Dispatcher | Conectarse al canal, consumir, deserializar, determinar tipo, despachar, acknowledge, error handling |
| Handler Registry | Mapear tipos de mensaje a handlers, registrar/desregistrar handlers |
| Handler | Procesar un tipo específico de mensaje con lógica de negocio |
| Fallback Handler | Manejar mensajes de tipos desconocidos o no registrados |
Interacciones¶
- Canal → Dispatcher: el dispatcher consume mensajes (pull o push según la plataforma).
- Dispatcher → Registry: consulta el handler correspondiente para cada tipo de mensaje.
- Dispatcher → Handler: invocación directa del método de procesamiento del handler.
- Handler → Dispatcher: retorno del resultado o excepción.
Contratos Implícitos¶
- Los mensajes deben contener un discriminador de tipo accesible al dispatcher.
- Los handlers deben implementar una interfaz o convención que el dispatcher pueda invocar.
- El handler no debe gestionar el acknowledge ni la conexión al broker — eso es responsabilidad del dispatcher.
Decisiones de Diseño Clave¶
- Mecanismo de tipo: ¿header del mensaje, campo en el payload, tipo de clase tras deserialización?
- Dispatch uno-a-uno vs. uno-a-muchos: ¿cada mensaje va a un solo handler o puede ir a múltiples?
- Concurrencia del dispatch: ¿secuencial o paralelo?
- Ciclo de vida de handlers: ¿singleton, per-message, scoped?
- Error isolation: ¿un handler fallido afecta el procesamiento de otros tipos?
8. Ejemplo Arquitectónico Detallado¶
Dominio: Telecomunicaciones — Procesamiento de CDR Events¶
Contexto del Negocio¶
Una operadora de telecomunicaciones procesa millones de CDR (Call Detail Records) diariamente. Cada CDR representa un evento de comunicación (llamada de voz, SMS, datos móviles, roaming) y debe ser procesado por múltiples subsistemas: tarificación (calcular el costo), auditoría regulatoria (registrar para cumplimiento normativo) y facturación (agregar al ciclo de facturación del cliente).
Necesidad de Integración¶
Los CDR llegan a un topic Kafka telecom.cdr.events desde múltiples fuentes (switches de red, paquetes de datos, gateways de SMS). Un servicio debe consumir estos eventos y despacharlos a tres handlers especializados dentro del mismo proceso, cada uno con lógica de negocio distinta.
Sistemas Involucrados¶
- Kafka Topic:
telecom.cdr.events— canal con CDR events de todos los tipos. - CDR Dispatcher Service: aplicación Java/Spring que consume del topic.
- Rate Calculator Handler: calcula el costo de cada CDR según el plan del cliente.
- Regulatory Audit Handler: genera registros de auditoría para el regulador.
- Billing Aggregator Handler: agrega el CDR al ciclo de facturación mensual.
- PostgreSQL: almacén de planes tarifarios.
- Elasticsearch: almacén de auditoría.
- Billing Database: almacén de cargos del ciclo de facturación.
Restricciones Técnicas¶
- Los CDR deben procesarse en orden por suscriptor (partition key = subscriber_id).
- Cada CDR debe pasar por los tres handlers (rating, audit, billing).
- Si el handler de rating falla, el CDR no debe facturarse (dependencia secuencial rating → billing).
- El handler de auditoría es independiente y no debe bloquear rating/billing.
- Throughput requerido: 10,000 CDR/segundo en picos.
Diseño del Dispatcher¶
CDR Dispatcher Architecture:
[Kafka Topic: telecom.cdr.events]
|
v
[CDR Dispatcher]
|-- inspects message type header
|
|-- type: "voice_call" → [Voice CDR Handler Pipeline]
| |-- Rate Calculator → Billing Aggregator
| |-- Regulatory Audit (parallel, independent)
|
|-- type: "sms" → [SMS CDR Handler Pipeline]
| |-- Rate Calculator → Billing Aggregator
| |-- Regulatory Audit (parallel, independent)
|
|-- type: "data_session" → [Data CDR Handler Pipeline]
| |-- Rate Calculator → Billing Aggregator
| |-- Regulatory Audit (parallel, independent)
|
|-- type: unknown → [Fallback Handler → Dead Letter]
Decisiones Arquitectónicas¶
- Dispatch por tipo de CDR: el header
cdr-typedel mensaje determina qué pipeline de handlers se invoca. Cada tipo de CDR tiene reglas de tarificación diferentes. - Pipeline secuencial para rating→billing: el costo calculado por el Rate Calculator es input del Billing Aggregator. Este dispatch es secuencial.
- Audit en paralelo: el Regulatory Audit Handler recibe el CDR en paralelo, sin depender del resultado de rating. Si audit falla, no bloquea la facturación.
- Fallback a dead-letter: CDR de tipos desconocidos se envían a un dead-letter topic para investigación.
Riesgos y Mitigaciones¶
| Riesgo | Mitigación |
|---|---|
| Handler de rating lento bloquea billing | Timeout por handler con circuit breaker |
| CDR de tipo desconocido se pierde | Fallback handler → dead-letter topic |
| Fallo en audit no detectado | Monitoring independiente de audit handler |
| Dispatcher como bottleneck | Pool de threads para dispatch paralelo por partición |
| Fallo total del dispatcher | Health checks + restart automático + consumer group rebalance |
9. Desarrollo Paso a Paso del Ejemplo¶
Paso 1: Recepción del CDR¶
Un suscriptor (ID: SUB-7829134) realiza una llamada de voz de 5 minutos desde Madrid a Barcelona. El switch de red genera un CDR que llega al topic telecom.cdr.events:
{
"cdr_id": "CDR-2026-04-07-18923847",
"cdr_type": "voice_call",
"subscriber_id": "SUB-7829134",
"timestamp": "2026-04-07T10:15:32Z",
"origin": "+34612345678",
"destination": "+34698765432",
"duration_seconds": 300,
"cell_id": "MAD-CENTRO-041",
"roaming": false,
"plan_id": "PLAN-ILIMITADO-50"
}
El mensaje llega con headers: cdr-type: voice_call, correlation-id: trace-abc-123.
Paso 2: Dispatch¶
El CDR Dispatcher consume el mensaje:
- Lee el header
cdr-type→voice_call. - Consulta el Handler Registry → encuentra
VoiceCallHandlerPipeline. - Propaga el
correlation-idal MDC (Mapped Diagnostic Context) para tracing. - Invoca el pipeline con el CDR deserializado.
Paso 3: Rate Calculator Handler¶
El Rate Calculator recibe el CDR de voz y ejecuta la tarificación:
- Consulta el plan
PLAN-ILIMITADO-50en PostgreSQL. - El plan incluye llamadas nacionales ilimitadas → costo = 0.00 EUR (incluido en plan).
- Registra el resultado:
{ rated_amount: 0.00, currency: "EUR", rating_code: "INCLUDED_IN_PLAN" }. - Retorna el CDR enriquecido con la tarificación al dispatcher.
Paso 4: Billing Aggregator Handler (secuencial tras rating)¶
El Billing Aggregator recibe el CDR ya tarificado:
- Aunque el costo es 0.00, registra el CDR en el ciclo de facturación del suscriptor para el detalle de consumo.
- Inserta en la Billing Database:
INSERT INTO billing_items (subscriber_id, cdr_id, amount, period) VALUES ('SUB-7829134', 'CDR-2026-04-07-18923847', 0.00, '2026-04'). - Retorna éxito.
Paso 5: Regulatory Audit Handler (en paralelo)¶
Simultáneamente al pipeline de rating→billing, el Regulatory Audit Handler:
- Recibe el CDR original.
- Genera un registro de auditoría con formato regulatorio (incluye origen, destino, duración, celda, timestamp).
- Indexa en Elasticsearch para queries de auditoría del regulador.
- Retorna éxito.
Paso 6: Acknowledge¶
El Dispatcher espera a que el pipeline secuencial (rating→billing) y el handler paralelo (audit) completen:
- Si ambos son exitosos → commit del offset en Kafka.
- Si rating o billing falla → no commit, el mensaje se reintentará.
- Si solo audit falla → commit (audit no es blocking), pero genera una alerta para retry manual.
10. Diagrama Técnico del Patrón¶
Código Python con diagrams¶
Ver / Copiar código de los diagramas
from diagrams import Diagram, Cluster, Edge
from diagrams.onprem.queue import Kafka
from diagrams.onprem.compute import Server
from diagrams.onprem.database import PostgreSQL
from diagrams.elastic.elasticsearch import Elasticsearch
from diagrams.onprem.database import Couchdb
with Diagram("Message Dispatcher - Telecom CDR Processing", show=False, direction="LR"):
with Cluster("Kafka"):
cdr_topic = Kafka("telecom.cdr.events")
dl_topic = Kafka("telecom.cdr.deadletter")
with Cluster("CDR Dispatcher Service"):
dispatcher = Server("CDR\nDispatcher")
with Cluster("Handler Registry"):
registry = Server("Handler\nRegistry")
with Cluster("Handlers"):
rate_handler = Server("Rate\nCalculator")
billing_handler = Server("Billing\nAggregator")
audit_handler = Server("Regulatory\nAudit")
fallback = Server("Fallback\nHandler")
with Cluster("Data Stores"):
plans_db = PostgreSQL("Plans &\nTariffs")
billing_db = PostgreSQL("Billing\nDatabase")
audit_store = Elasticsearch("Audit\nIndex")
# Flow
cdr_topic >> dispatcher
dispatcher >> Edge(label="lookup") >> registry
dispatcher >> Edge(label="voice/sms/data") >> rate_handler
rate_handler >> Edge(label="rated CDR") >> billing_handler
dispatcher >> Edge(label="all types\n(parallel)") >> audit_handler
dispatcher >> Edge(style="dashed", label="unknown type") >> fallback
fallback >> dl_topic
rate_handler >> plans_db
billing_handler >> billing_db
audit_handler >> audit_store
from diagrams import Diagram, Cluster, Edge
from diagrams.aws.compute import Lambda
from diagrams.aws.database import RDS, Dynamodb
from diagrams.aws.integration import SQS, Eventbridge
from diagrams.aws.analytics import ElasticsearchService
with Diagram("Message Dispatcher - Telecom CDR Processing (AWS)", show=False, direction="LR"):
with Cluster("Event Source"):
cdr_queue = SQS("cdr-events\nQueue")
dlq = SQS("cdr-deadletter\nDLQ")
with Cluster("CDR Dispatcher Service"):
dispatcher = Lambda("CDR\nDispatcher")
with Cluster("EventBridge Rules"):
event_bus = Eventbridge("CDR\nEvent Bus")
with Cluster("Handlers"):
rate_handler = Lambda("Rate\nCalculator")
billing_handler = Lambda("Billing\nAggregator")
audit_handler = Lambda("Regulatory\nAudit")
fallback = Lambda("Fallback\nHandler")
with Cluster("Data Stores"):
plans_db = Dynamodb("Plans &\nTariffs")
billing_db = RDS("Billing\nDatabase")
audit_store = ElasticsearchService("OpenSearch\nAudit Index")
# Flow
cdr_queue >> dispatcher
dispatcher >> Edge(label="route by type") >> event_bus
event_bus >> Edge(label="voice/sms/data") >> rate_handler
rate_handler >> Edge(label="rated CDR") >> billing_handler
event_bus >> Edge(label="all types\n(parallel)") >> audit_handler
event_bus >> Edge(style="dashed", label="unknown type") >> fallback
fallback >> dlq
rate_handler >> plans_db
billing_handler >> billing_db
audit_handler >> audit_store
from diagrams import Diagram, Cluster, Edge
from diagrams.azure.compute import FunctionApps
from diagrams.azure.database import SQLDatabases, CosmosDb
from diagrams.azure.integration import ServiceBus
from diagrams.azure.analytics import LogAnalyticsWorkspaces
with Diagram("Message Dispatcher - Telecom CDR Processing (Azure)", show=False, direction="LR"):
with Cluster("Azure Service Bus"):
cdr_topic = ServiceBus("cdr-events\nTopic")
dlq = ServiceBus("Dead Letter\nQueue")
with Cluster("CDR Dispatcher Function"):
dispatcher = FunctionApps("CDR Dispatcher\n(SB Trigger +\ntype-based routing)")
with Cluster("Handlers"):
rate_handler = FunctionApps("Rate\nCalculator")
billing_handler = FunctionApps("Billing\nAggregator")
audit_handler = FunctionApps("Regulatory\nAudit")
fallback = FunctionApps("Fallback\nHandler")
with Cluster("Data Stores"):
plans_db = CosmosDb("Plans &\nTariffs")
billing_db = SQLDatabases("Azure SQL\nBilling")
audit_store = LogAnalyticsWorkspaces("Log Analytics\nAudit Index")
# Flow
cdr_topic >> Edge(label="trigger") >> dispatcher
dispatcher >> Edge(label="voice/sms/data") >> rate_handler
rate_handler >> Edge(label="rated CDR") >> billing_handler
dispatcher >> Edge(label="all types\n(parallel)") >> audit_handler
dispatcher >> Edge(style="dashed", label="unknown type") >> fallback
fallback >> dlq
rate_handler >> plans_db
billing_handler >> billing_db
audit_handler >> audit_store
Explicación del Diagrama¶
El diagrama muestra la arquitectura del Message Dispatcher para procesamiento de CDR en telecomunicaciones:
- Los CDR llegan del Kafka Topic al CDR Dispatcher, que es el único consumidor conectado al broker.
- El Dispatcher consulta el Handler Registry para encontrar el handler correspondiente al tipo del CDR.
- Para CDR de tipo conocido (voice, SMS, data), el Dispatcher invoca el Rate Calculator que tarifica el CDR consultando la base de datos de Plans & Tariffs.
- El CDR tarificado pasa al Billing Aggregator que lo registra en la Billing Database.
- En paralelo, el Regulatory Audit handler indexa el CDR en Elasticsearch.
- Para CDR de tipo desconocido, el Fallback Handler los envía al dead-letter topic.
Correspondencia Patrón ↔ Diagrama¶
| Concepto del Patrón | Componente del Diagrama |
|---|---|
| Message Channel | Kafka Topic telecom.cdr.events |
| Dispatcher | CDR Dispatcher |
| Handler Registry | Handler Registry |
| Performer/Handler | Rate Calculator, Billing Aggregator, Regulatory Audit |
| Fallback Handler | Fallback Handler → Dead Letter Topic |
| Discriminador de tipo | Header cdr-type en el mensaje |
11. Beneficios¶
Impacto Técnico¶
- Desacoplamiento de infraestructura: los handlers de tarificación, auditoría y facturación no contienen código de Kafka. Son POJO/POCO puros que reciben un objeto CDR y retornan un resultado. Se testean con JUnit sin broker.
- Extensibilidad (Open/Closed): añadir soporte para un nuevo tipo de CDR (por ejemplo,
video_call) solo requiere crear un nuevo handler y registrarlo. No se modifica el dispatcher ni los handlers existentes. - Cross-cutting centralizado: logging, métricas, tracing y error handling se implementan una sola vez en el dispatcher. Todos los handlers se benefician automáticamente.
- Punto único de acknowledge: la lógica de cuándo hacer commit del offset está en un solo lugar, no distribuida en cada handler.
Impacto Organizacional¶
- Separación de responsabilidades: el equipo de infraestructura mantiene el dispatcher; los equipos de negocio mantienen sus handlers. La interfaz entre ambos es un contrato simple.
- Onboarding: un nuevo desarrollador puede crear un handler sin conocer Kafka, Spring Kafka o los detalles de la deserialización.
- Code reviews: los handlers son pequeños, focalizados y fáciles de revisar.
Impacto Operacional¶
- Monitoreo granular: el dispatcher puede emitir métricas por tipo de mensaje (latencia, throughput, error rate por handler).
- Single connection: una sola conexión al broker en lugar de una por handler, reduciendo recursos.
- Configuración centralizada: retry policies, timeouts y circuit breakers se configuran en un solo lugar.
Beneficios de Mantenibilidad y Evolución¶
- Testabilidad: cada handler se testea unitariamente con mocks simples.
- Refactoring: la lógica de un handler se puede reescribir sin afectar a los demás.
- Migración gradual: si se decide mover un handler a un microservicio independiente, se puede reemplazar el handler local por un proxy que envía el mensaje a otro canal.
12. Desventajas y Riesgos¶
Complejidad Añadida¶
- Capa adicional: el dispatcher es un componente más que mantener, testear y debuggear. En aplicaciones simples con un solo tipo de mensaje, es overhead innecesario.
- Dispatch resolution: el mecanismo de resolución de handlers (registry, annotations, reflection) añade complejidad y puede ser difícil de debuggear cuando no se encuentra un handler.
- Flujo menos visible: el flujo del mensaje no es evidente en el código; requiere entender el mecanismo de dispatch para seguir la ejecución.
Riesgos de Mal Uso¶
- Dispatcher como God Object: si el dispatcher acumula demasiada lógica (validación, transformación, enriquecimiento) además del dispatch, se convierte en un componente monolítico difícil de mantener.
- Handler coupling: handlers que dependen de resultados de otros handlers (como rating→billing) introducen acoplamiento secuencial que el dispatcher debe gestionar, complicando el flujo.
- Fallback silencioso: un fallback handler que solo loguea y descarta mensajes desconocidos puede ocultar problemas de deserialización o tipos de mensaje no registrados.
Sobreingeniería¶
- Dispatch framework propio: construir un framework de dispatch sofisticado cuando Spring, MassTransit o NServiceBus ya proporcionan esta funcionalidad es reinventar la rueda.
- Dynamismo innecesario: registrar handlers dinámicamente en runtime cuando los tipos de mensaje son estáticos y conocidos en compile time.
Costos de Operación¶
- Single point of failure: si el dispatcher falla, ningún handler procesa mensajes. Requiere health checks y restart automático.
- Bottleneck potencial: si el dispatch es síncrono, un handler lento bloquea todo el procesamiento.
Anti-Patterns Relacionados¶
- Mega Dispatcher: un dispatcher que maneja 50+ tipos de mensaje con lógica de routing compleja, convirtiéndose en un router embebido cuando debería haber múltiples consumidores o un Content-Based Router externo.
- Leaky Dispatcher: handlers que acceden directamente al broker para enviar mensajes de respuesta, bypaseando el dispatcher y rompiendo el encapsulamiento.
13. Relación con Otros Patrones¶
Patrones Complementarios¶
- Event-Driven Consumer: el dispatcher típicamente es un Event-Driven Consumer que recibe mensajes por push y los despacha internamente.
- Messaging Gateway: el dispatcher actúa como un gateway interno que oculta la infraestructura de messaging a los handlers.
- Service Activator: cada handler es esencialmente un Service Activator — lógica de servicio invocada por la llegada de un mensaje.
- Content-Based Router: Message Dispatcher es la versión in-process del Content-Based Router. Ambos inspeccionan el contenido del mensaje y lo dirigen a un destino apropiado, pero el Router opera entre canales y el Dispatcher opera entre handlers dentro de un proceso.
Patrones que Suelen Aparecer Antes o Después¶
- Antes: Message Channel define el canal del que el dispatcher consume. Messaging Mapper define cómo se convierten los mensajes a objetos de dominio.
- Después: los handlers pueden usar Messaging Gateway para enviar mensajes de respuesta o Command Message para invocar otros servicios.
Combinaciones Comunes¶
- Message Dispatcher + Competing Consumers: múltiples instancias del dispatcher (consumer group) compiten por mensajes, y cada instancia despacha internamente a sus handlers.
- Message Dispatcher + Idempotent Receiver: los handlers deben ser idempotentes porque el dispatcher puede reintentar un mensaje.
- Message Dispatcher + Transactional Client: el dispatcher gestiona la transacción que incluye el procesamiento del handler y el acknowledge del mensaje.
Diferencias con Patrones Similares¶
- vs. Content-Based Router: el router opera entre canales (inter-process), el dispatcher opera entre handlers (intra-process).
- vs. Selective Consumer: el selective consumer filtra qué mensajes recibir del canal; el dispatcher recibe todos y distribuye internamente.
- vs. Competing Consumers: los competing consumers son instancias paralelas del mismo consumidor; el dispatcher es un solo consumidor con múltiples handlers.
Encaje en un Flujo Mayor de Integración¶
Message Dispatcher es un patrón de organización interna del endpoint. Se combina con patrones de canal (Message Channel) y patrones de routing (Content-Based Router) para formar una arquitectura completa donde los mensajes fluyen por canales, se enrutan entre servicios y se despachan a handlers dentro de cada servicio.
14. Relevancia Actual del Patrón¶
Evaluación: Relevancia Media¶
Argumentación¶
Message Dispatcher sigue siendo un patrón conceptualmente importante pero cuya implementación explícita es cada vez menos frecuente, porque los frameworks modernos lo implementan automáticamente:
- Spring Kafka:
@KafkaListenercon@KafkaHandleren una clase multi-tipo es un dispatcher implícito. - MassTransit: el framework resuelve consumers por tipo de mensaje automáticamente.
- NServiceBus:
IHandleMessages<T>es un registro de handler que el framework despacha. - Azure Functions: bindings + routing por tipo es dispatch automático.
- AWS Lambda: el mapeo de evento → handler es dispatch por configuración.
Qué Parte Sigue Siendo Esencial¶
- El concepto de separar recepción de procesamiento: aunque el framework haga el dispatch, el arquitecto debe entender que existe esta separación y diseñar los handlers correctamente.
- El diseño de handlers desacoplados: escribir handlers como componentes puros de negocio sin dependencias de infraestructura sigue siendo una best practice fundamental.
- La gestión de cross-cutting concerns en el dispatcher: entender que el middleware del framework (interceptors, filters) cumple la función del dispatcher permite configurarlos correctamente.
Cómo Se Implementa Hoy¶
| Plataforma | Implementación del Dispatch | Mecanismo |
|---|---|---|
| Spring Kafka | @KafkaHandler por tipo | Annotations + reflection |
| MassTransit (.NET) | IConsumer<T> por tipo | DI container + type resolution |
| NServiceBus | IHandleMessages<T> | Convention-based resolution |
| Azure Functions | Trigger + route | Configuration-based |
| Quarkus | @Incoming + CDI | Annotations + CDI |
15. Implementación en Arquitecturas Modernas¶
Spring Kafka (Java)¶
@Component
@KafkaListener(topics = "telecom.cdr.events", groupId = "cdr-processor")
public class CdrDispatcher {
@KafkaHandler
public void handleVoiceCall(VoiceCallCdr cdr) {
// dispatch automático por tipo deserializado
}
@KafkaHandler
public void handleSms(SmsCdr cdr) {
// dispatch automático por tipo deserializado
}
@KafkaHandler(isDefault = true)
public void handleUnknown(Object cdr) {
// fallback handler
}
}
Spring Kafka usa el type header del mensaje para deserializar al tipo correcto y despachar al método @KafkaHandler cuyo parámetro coincide.
MassTransit (.NET)¶
public class VoiceCallCdrConsumer : IConsumer<VoiceCallCdr>
{
public async Task Consume(ConsumeContext<VoiceCallCdr> context)
{
// handler específico para VoiceCallCdr
}
}
// Registro en startup
services.AddMassTransit(x => {
x.AddConsumer<VoiceCallCdrConsumer>();
x.AddConsumer<SmsCdrConsumer>();
x.AddConsumer<DataSessionCdrConsumer>();
});
MassTransit resuelve el consumer correcto por el tipo del mensaje deserializado, actuando como dispatcher automático.
Custom Dispatcher (cualquier plataforma)¶
class MessageDispatcher:
def __init__(self):
self._handlers = {}
self._fallback = None
def register(self, message_type: str, handler: Callable):
self._handlers[message_type] = handler
def set_fallback(self, handler: Callable):
self._fallback = handler
def dispatch(self, message: dict):
msg_type = message.get("type", "unknown")
handler = self._handlers.get(msg_type, self._fallback)
if handler:
return handler(message)
raise UnhandledMessageError(f"No handler for type: {msg_type}")
Serverless (AWS Lambda)¶
En serverless, el dispatch se hace por configuración: cada tipo de evento tiene su propia Lambda function mapeada en el event source mapping. El "dispatcher" es la plataforma misma.
16. Consideraciones de Gobierno y Operación¶
Observabilidad¶
- Métricas por handler: el dispatcher debe emitir métricas de latencia, throughput y error rate por handler y por tipo de mensaje.
- Distributed tracing: el dispatcher debe propagar trace context (W3C Trace Context, OpenTelemetry) a cada handler.
- Logging estructurado: cada dispatch debe loguearse con message_id, message_type, handler_name, duration, outcome.
Monitoreo¶
- Handler health: monitorear si cada handler está respondiendo dentro de los SLAs.
- Unknown message types: alertar cuando llegan mensajes de tipos no registrados (indicador de desincronización entre productor y consumidor).
- Dispatch latency: monitorear la latencia del dispatch itself (separada de la latencia del handler).
Seguridad¶
- Handler isolation: asegurar que un handler comprometido no pueda acceder a datos de otro handler.
- Input validation: el dispatcher debe validar el mensaje antes de pasarlo al handler para evitar inyección.
Manejo de Errores¶
- Retry por handler: configurar políticas de retry independientes por handler (el handler de auditoría puede tolerar más retries que el de facturación).
- Circuit breaker por handler: si un handler falla repetidamente, abrirlo para evitar acumulación de lag.
- Dead-lettering: mensajes que agotan retries van a un dead-letter topic con metadata del handler que falló.
Performance¶
- Thread pool sizing: el pool de threads para dispatch paralelo debe dimensionarse según el número de handlers y su latencia esperada.
- Batch dispatch: si el framework lo soporta, despachar batches de mensajes del mismo tipo a un handler reduce overhead.
- Prewarm: instanciar handlers al startup, no en el primer dispatch, para evitar latencia de cold start.
Escalabilidad¶
- La escalabilidad del dispatcher se logra con Competing Consumers: múltiples instancias del mismo servicio (consumer group) donde cada instancia tiene su propio dispatcher.
- El número de instancias está limitado por el número de particiones del canal.
17. Errores Comunes¶
Construir un Dispatcher Cuando el Framework Ya Lo Proporciona¶
El error más frecuente es implementar un dispatcher manual con un switch/case sobre el tipo de mensaje cuando Spring, MassTransit o NServiceBus ya proporcionan dispatch automático por tipo. El resultado es código redundante, menos robusto que la solución del framework.
No Implementar Fallback Handler¶
Cuando llega un mensaje de un tipo que no tiene handler registrado (por ejemplo, un tipo nuevo añadido por el productor sin coordinación con el consumidor), sin fallback el mensaje se descarta silenciosamente o causa una excepción no manejada. Siempre debe existir un handler de fallback que al menos loguee y envíe a dead-letter.
Dispatch Síncrono Sin Timeout¶
Un handler lento o bloqueado que no tiene timeout bloquea el dispatcher indefinidamente. Todos los demás tipos de mensaje se acumulan en el canal sin procesarse. Los timeouts por handler son obligatorios.
Handlers con Dependencias de Infraestructura¶
Handlers que importan directamente el SDK de Kafka, crean sus propias conexiones al broker o gestionan offsets manualmente derrotan el propósito del dispatcher. Los handlers deben ser componentes de negocio puros.
No Monitorear por Tipo de Mensaje¶
Monitorear solo el throughput total del dispatcher sin desglosar por tipo de mensaje oculta problemas. Un handler con error rate del 100% puede estar oculto por los otros handlers que procesan exitosamente.
Mega Dispatcher¶
Un dispatcher que maneja decenas de tipos de mensaje con lógica de routing compleja, transformaciones intermedias y orquestación de handlers se convierte en un monolito dentro del monolito. Cuando la complejidad del dispatch crece, es momento de dividir en consumidores separados o usar un Content-Based Router externo.
18. Conclusión Técnica¶
Message Dispatcher es un patrón de organización interna del endpoint que separa la recepción de mensajes de su procesamiento, creando una arquitectura limpia donde los handlers contienen lógica de negocio pura y el dispatcher gestiona la infraestructura de messaging. Es el análogo in-process del Content-Based Router: inspecciona el tipo del mensaje y lo dirige al handler apropiado.
Cuándo aporta valor: en cualquier aplicación que consume mensajes de tipos heterogéneos de uno o pocos canales. El valor está en el desacoplamiento de los handlers respecto a la infraestructura de messaging, la centralización de cross-cutting concerns y la extensibilidad del sistema de handlers.
Cuándo evita problemas importantes: cuando el número de tipos de mensaje crece, un dispatcher bien diseñado (o el mecanismo de dispatch del framework) evita la explosión de complejidad que se produce cuando cada handler gestiona su propia conexión, deserialización y error handling.
Cuándo no conviene adoptarlo: en aplicaciones con un solo tipo de mensaje, o donde cada tipo tiene su propio canal y consumidor independiente. En esos casos, la indirección del dispatcher no añade valor.
Recomendación para arquitectos: antes de implementar un dispatcher manual, verifique que su framework no lo proporcione ya. Spring @KafkaHandler, MassTransit IConsumer<T> y NServiceBus IHandleMessages<T> son dispatchers incorporados. Si debe implementar uno propio, mantenga el dispatcher delgado (solo dispatch, no lógica de negocio), implemente fallback handling, configure timeouts por handler y emita métricas por tipo de mensaje. El dispatcher es infraestructura, no negocio.


