Command Message¶
1. Nombre del Patrón¶
- Nombre oficial: Command Message
- Categoría: Message Construction (Construcción de Mensajes)
- Traducción contextual: Mensaje de Comando
2. Resumen Ejecutivo¶
Command Message es un patrón de construcción de mensajes que define cómo estructurar un mensaje cuya semántica es imperativa: el productor instruye al consumidor a ejecutar una acción específica. El mensaje no informa ni transfiere datos — ordena. Es el equivalente asíncrono de una invocación de método remoto, pero con las ventajas de desacoplamiento temporal y espacial que proporciona el messaging.
El problema que resuelve es fundamental: en un sistema distribuido basado en mensajería, ¿cómo expresa el productor que necesita que el consumidor haga algo concreto? No basta con enviar datos; el mensaje debe comunicar inequívocamente la intención de que se ejecute una acción. Command Message establece la convención de que el mensaje tiene semántica imperativa, que existe un único receptor autorizado para ejecutarlo y que el productor tiene expectativa de que el comando se ejecute (aunque no necesariamente espere una respuesta síncrona).
Este patrón es absolutamente central en las arquitecturas modernas. Los command buses de CQRS, las task queues de sistemas como Celery o Sidekiq, los comandos de sagas distribuidas y las invocaciones asíncronas entre microservicios son todos implementaciones de Command Message. Comprender su semántica — y distinguirla claramente de Event Message y Document Message — es una de las decisiones de diseño más impactantes en una arquitectura event-driven.
3. Definición Detallada¶
Propósito¶
Command Message establece un contrato semántico en el que el productor envía un mensaje que expresa una instrucción directa al consumidor. El mensaje dice "haz esto" — no "esto ocurrió" ni "aquí tienes datos". El propósito es modelar intenciones de acción como mensajes, permitiendo que esas acciones se ejecuten de forma asíncrona, desacoplada y potencialmente retriable.
Lógica Arquitectónica¶
En una invocación síncrona (REST POST, gRPC call), la semántica imperativa es obvia: el caller invoca un endpoint y espera un resultado. En messaging, la semántica no es inherente al mecanismo de transporte — un mensaje es simplemente un payload con headers. Command Message establece la convención de que ciertos mensajes representan instrucciones, y que esta convención tiene consecuencias arquitectónicas:
- Un solo consumidor ejecuta el comando: a diferencia de un evento que puede tener múltiples suscriptores, un comando se envía a un único receptor responsable de ejecutarlo. Esto implica canales point-to-point.
- El productor conoce al consumidor (conceptualmente): aunque no necesita conocer su dirección física, el productor sabe qué tipo de servicio procesará el comando. Hay acoplamiento semántico intencional.
- Existe expectativa de ejecución: el productor espera que el comando se ejecute. Si falla, debe haber mecanismos de reintento, compensación o notificación de fallo.
- El comando puede rechazarse: a diferencia de un evento (que es un hecho consumado), un comando puede ser validado y rechazado por el receptor si no cumple precondiciones.
Principio de Diseño Subyacente¶
El principio es separación de la decisión de actuar de la ejecución de la acción. El productor decide que una acción debe ocurrir y la expresa como un mensaje. El consumidor es responsable de ejecutarla. Esta separación permite que la decisión y la ejecución ocurran en diferentes tiempos, diferentes procesos, diferentes servicios y diferentes nodos.
Problema Estructural que Resuelve¶
En sistemas distribuidos, las acciones que cruzan fronteras de servicio (transferir dinero, enviar un email, aprobar una solicitud) requieren que un servicio instruya a otro. Sin Command Message, las opciones son:
- Invocación síncrona directa: el servicio A llama al servicio B. Acoplamiento temporal — si B no está disponible, la acción no puede iniciarse.
- Modelo ambiguo: enviar un mensaje genérico que el receptor debe interpretar como "debes hacer algo" sin una convención clara.
Command Message proporciona un modelo explícito y estandarizado para expresar intenciones de acción a través de messaging.
Contexto en el que Emerge¶
Command Message emerge cuando un servicio necesita delegar la ejecución de una acción a otro servicio de forma asíncrona. Es especialmente frecuente en:
- CQRS: el lado de comandos recibe Command Messages que expresan intenciones de modificar estado.
- Sagas y orquestación: un orquestador envía comandos a los participantes de una transacción distribuida.
- Task queues: un producer encola tareas (commands) que workers ejecutan.
- Asynchronous APIs: cuando una API acepta una petición y la encola para procesamiento diferido.
Diferencia Fundamental: Commands vs. Events¶
Esta distinción es una de las más importantes en el diseño de arquitecturas event-driven:
| Aspecto | Command Message | Event Message |
|---|---|---|
| Semántica | Imperativa: "Haz esto" | Declarativa: "Esto ocurrió" |
| Tiempo verbal | Imperativo/Futuro: TransferFunds, SendEmail | Pasado: FundsTransferred, EmailSent |
| Destinatarios | Exactamente uno | Cero o muchos |
| Canal típico | Point-to-Point (Queue) | Publish-Subscribe (Topic) |
| Puede rechazarse | Sí | No (es un hecho consumado) |
| Acoplamiento | Productor conoce al consumidor (tipo) | Productor no sabe quién escucha |
| Fallo | Debe manejarse (retry, compensación) | Los suscriptores manejan individualmente |
Relación con Sistemas Distribuidos y Mensajería¶
En la teoría de sistemas distribuidos, Command Message corresponde al concepto de un reliable one-to-one message delivery con at-least-once semantics. El productor necesita garantía de que el comando será procesado, lo cual requiere persistencia del mensaje, acknowledgment del consumidor y mecanismos de reintento.
En la práctica, Command Message se implementa sobre canales point-to-point (queues) con garantía de entrega:
- En RabbitMQ: una queue durable con message acknowledgment.
- En Kafka: un topic con un único consumer group (emulando point-to-point).
- En AWS SQS: una queue estándar o FIFO con visibility timeout.
- En Azure Service Bus: una queue con sessions para orden garantizado por entidad.
4. Problema que Resuelve¶
El Problema Antes del Patrón¶
Sin Command Message como abstracción explícita, los sistemas que necesitan delegar acciones a otros servicios enfrentan:
- Ambigüedad semántica: un mensaje llega a un servicio, pero no está claro si debe ejecutar una acción, almacenar datos o simplemente tomar nota de una notificación. La falta de convención semántica produce interpretaciones inconsistentes.
- Acoplamiento síncrono: sin un mecanismo asíncrono de comandos, el servicio que necesita una acción debe llamar síncronamente al servicio ejecutor. Si el ejecutor no está disponible, la acción se pierde o el servicio llamador se bloquea.
- Sin garantía de ejecución: en una llamada síncrona que falla, el retry depende del llamador. Si el llamador falla después de la llamada pero antes de registrar el resultado, la acción puede ejecutarse dos veces o no ejecutarse nunca.
- Imposibilidad de priorización y throttling: sin una queue de comandos, todas las peticiones se procesan con la misma prioridad y al ritmo que llegan, sin posibilidad de priorizar, limitar o diferir.
Síntomas del Problema¶
- Servicios que fallan en cascada cuando un servicio downstream no está disponible, porque todas las interacciones son síncronas.
- Acciones que se pierden durante ventanas de mantenimiento o picos de carga porque no hay buffer intermedio.
- Mensajes que se procesan de forma inconsistente porque algunos desarrolladores los interpretan como comandos y otros como notificaciones.
- Imposibilidad de implementar retry con backoff porque no hay queue que almacene los comandos pendientes.
- Duplicación de lógica de acción porque, sin un comando explícito, cada servicio reimplementa la misma acción localmente.
Impacto Operativo y Arquitectónico¶
Sin Command Message:
- Las acciones entre servicios se pierden silenciosamente durante fallos parciales.
- El throughput del sistema está limitado por la capacidad del servicio más lento en la cadena síncrona.
- No hay visibilidad sobre cuántos comandos están pendientes, cuántos se ejecutaron y cuántos fallaron.
- La evolución del sistema (añadir validaciones, cambiar el servicio ejecutor, implementar aprobaciones) requiere cambios en todos los servicios que invocan la acción.
Riesgos Si No Se Implementa Correctamente¶
- Comando sin idempotencia: si el consumidor no es idempotente, reintentos legítimos producen ejecuciones duplicadas (doble transferencia, doble envío de email).
- Comando sin validación: aceptar un comando sin validar precondiciones puede dejar el sistema en un estado inconsistente.
- Comando sin response channel: si el productor necesita saber si el comando se ejecutó, la falta de un mecanismo de respuesta produce incertidumbre operativa.
- Comando en canal pub-sub: enviar un comando a un topic pub-sub hace que múltiples consumidores lo ejecuten, produciendo duplicación de acciones.
Ejemplos Reales¶
- Banca: un comando
InitiateWireTransferenviado desde el servicio de banca online al servicio de procesamiento de pagos. El comando debe ejecutarse exactamente una vez, debe ser validable (fondos suficientes, cuenta activa) y debe generar una respuesta (éxito o rechazo). - E-commerce: un comando
ReserveInventoryenviado desde el servicio de pedidos al servicio de inventario como parte de una saga de compra. Si el inventario no está disponible, el comando se rechaza y la saga compensa. - Healthcare: un comando
ScheduleLabTestenviado desde el sistema de órdenes médicas al sistema de laboratorio. El comando incluye todos los datos necesarios para programar la prueba.
5. Contexto de Aplicación¶
Cuándo Usarlo¶
- Cuando un servicio necesita que otro servicio ejecute una acción específica de forma asíncrona.
- Cuando la acción puede fallar, ser validada o rechazada, y esta información es relevante para el productor.
- Cuando se necesita garantía de que la acción se procesará eventualmente (at-least-once delivery).
- Cuando se quiere desacoplar temporalmente al servicio que decide la acción del servicio que la ejecuta.
- Cuando se necesita priorización, throttling o scheduling de acciones.
- En el lado de escritura de CQRS, donde los comandos expresan intenciones de cambio de estado.
- En sagas y orquestación de transacciones distribuidas, donde el orquestador envía comandos a los participantes.
Cuándo No Usarlo¶
- Cuando la información es una notificación sin expectativa de acción — usar Event Message.
- Cuando el productor envía datos para que el consumidor los use como necesite — usar Document Message.
- Cuando el productor necesita una respuesta inmediata síncrona y la latencia del messaging no es aceptable — usar RPC directo.
- Cuando múltiples consumidores deben reaccionar al mismo mensaje — eso es semántica de evento, no de comando.
- Cuando no hay un consumidor específico responsable de ejecutar la acción — la semántica de comando requiere un receptor definido.
Precondiciones¶
- Existe un canal point-to-point (queue) para transportar los comandos.
- El consumidor está identificado y es responsable de ejecutar el tipo de comando.
- Existe un contrato (explícito o implícito) sobre la estructura del comando y las precondiciones para su ejecución.
- El consumidor implementa idempotencia si la entrega es at-least-once.
Restricciones¶
- Un Command Message tiene exactamente un destinatario lógico. Si se necesita que múltiples servicios actúen, se envían múltiples comandos o se usa un orquestador.
- El productor asume acoplamiento semántico: sabe qué tipo de servicio procesará el comando.
- El comando puede ser rechazado; el productor debe tener una estrategia para manejar rechazos.
Dependencias¶
- Canal point-to-point con garantía de entrega (durable queue).
- Mecanismo de serialización/deserialización del comando (JSON Schema, Avro, Protobuf).
- Opcionalmente, un Return Address y Correlation Identifier si se necesita respuesta.
- Opcionalmente, un Dead Letter Channel para comandos que no pueden procesarse.
Supuestos Arquitectónicos¶
- El servicio consumidor existe, está desplegado y eventualmente procesará el comando.
- La entrega at-least-once es aceptable (con idempotencia del consumidor).
- El productor puede tolerar latencia entre el envío del comando y su ejecución.
Tipo de Sistemas Donde Aparece con Más Frecuencia¶
- Sistemas bancarios y financieros (transferencias, pagos, aprobaciones).
- E-commerce (pedidos, reservas, envíos).
- Sistemas de workflow y BPM (aprobaciones, escalaciones).
- Arquitecturas CQRS/ES (command side).
- Orquestación de sagas distribuidas.
- Task queues y job processing (Celery, Sidekiq, Bull).
6. Fuerzas Arquitectónicas¶
Acoplamiento vs. Flexibilidad¶
Command Message introduce un acoplamiento semántico intencional entre productor y consumidor: el productor sabe qué tipo de acción debe ejecutarse y, por tanto, qué tipo de servicio la procesará. Esto es más acoplamiento que un Event Message (donde el productor no sabe quién escucha), pero menos acoplamiento que una invocación síncrona directa (donde el productor necesita la dirección exacta del consumidor). El trade-off es deliberado: la semántica imperativa requiere que alguien sea responsable de ejecutar la acción.
Sincronía vs. Asincronía¶
Command Message convierte una operación potencialmente síncrona (llamar a un servicio para que haga algo) en una operación asíncrona (encolar un mensaje con la instrucción). Esto mejora la resiliencia y el throughput, pero introduce latencia y complejidad: el productor no sabe inmediatamente si el comando se ejecutó exitosamente. Si necesita saberlo, debe implementar Request-Reply o un mecanismo de callback.
Garantía de Ejecución vs. Simplicidad¶
Garantizar que un comando se ejecute exactamente una vez en un sistema distribuido es el problema de exactly-once delivery, que es fundamentalmente difícil. La aproximación práctica es at-least-once delivery con idempotencia: el broker garantiza que el comando se entregará al menos una vez, y el consumidor garantiza que ejecutarlo múltiples veces produce el mismo resultado que ejecutarlo una vez.
Validación en Productor vs. Validación en Consumidor¶
¿Dónde se valida un comando? Si el productor valida exhaustivamente, evita enviar comandos inválidos pero duplica lógica de negocio. Si solo el consumidor valida, el productor puede enviar comandos que serán rechazados, desperdiciando recursos. La práctica recomendada es validación estructural en el productor (formato correcto, campos obligatorios) y validación de negocio en el consumidor (precondiciones, estado actual).
Tamaño del Comando vs. Eficiencia¶
¿El comando debe contener todos los datos necesarios para su ejecución (self-contained) o solo una referencia a los datos (claim check)? Un comando self-contained es más simple de procesar pero puede ser grande. Un comando con referencias es más pequeño pero requiere que el consumidor consulte otros servicios para obtener los datos, introduciendo dependencias adicionales.
Orden vs. Paralelismo¶
Algunos comandos deben procesarse en orden (por ejemplo, comandos que modifican la misma entidad). Otros pueden procesarse en paralelo. Garantizar orden global limita el paralelismo. La solución habitual es garantizar orden por partition key (por ejemplo, por account_id), permitiendo paralelismo entre diferentes entidades.
7. Estructura Conceptual del Patrón¶
Actores o Componentes Involucrados¶
- Command Issuer (Emisor del Comando): el servicio que decide que una acción debe ejecutarse y construye el Command Message.
- Command Queue (Cola de Comandos): el canal point-to-point que almacena y transporta el comando hasta que el handler lo procese.
- Command Handler (Manejador del Comando): el servicio responsable de recibir el comando, validarlo y ejecutar la acción.
- Response Channel (Canal de Respuesta): opcionalmente, un canal por donde el handler notifica al emisor el resultado de la ejecución.
- Dead Letter Queue: canal donde se depositan comandos que no pueden procesarse después de los reintentos configurados.
Flujo Lógico¶
flowchart TD
A([Command Issuer]) --> B[Construir Command Message\ncomando + payload + metadata]
B --> C[(Command Queue)]
C --> D[Broker almacena mensaje\npersistente y durable]
D --> E[Command Handler\nconsume mensaje]
E --> F{Precondiciones\nválidas?}
F -- No --> G[Rechazar mensaje\nnack o respuesta de error]
F -- Sí --> H[Ejecutar la acción]
H --> I[Confirmar procesamiento\nack]
I --> J{Requiere\nrespuesta?}
J -- Sí --> K[(Response Channel)]
J -- No --> L([Fin])
K --> L
G --> L Responsabilidades¶
| Componente | Responsabilidad |
|---|---|
| Command Issuer | Construir comando con datos completos, asignar command_id único, enviar al canal correcto |
| Command Queue | Almacenar comando durablemente, entregarlo a exactamente un handler, soportar retry |
| Command Handler | Validar precondiciones, ejecutar acción idempotentemente, confirmar o rechazar |
| Response Channel | Transportar resultado de vuelta al issuer (si aplica) |
| Dead Letter Queue | Almacenar comandos que fallaron todos los reintentos para inspección manual |
Interacciones¶
- Issuer → Queue: operación de envío (send/produce) con confirmación del broker.
- Queue → Handler: entrega del comando con visibility timeout o lock.
- Handler → Queue: acknowledgment de procesamiento exitoso o negative acknowledgment de fallo.
- Handler → Response Channel: envío opcional del resultado.
Contratos Implícitos¶
- Nombre del comando: define la acción esperada (
InitiateWireTransfer,SendNotification). - Schema del payload: los campos necesarios para ejecutar la acción y sus tipos.
- Precondiciones: qué estado debe existir para que el comando sea válido.
- Idempotencia: el handler debe producir el mismo resultado si recibe el mismo comando más de una vez.
Decisiones de Diseño Clave¶
- Naming convention: los comandos se nombran en imperativo (
TransferFunds,CancelOrder,ApproveRequest) — nunca en pasado. - Self-contained vs. reference: ¿el comando incluye todos los datos o solo referencias?
- Response mechanism: ¿el issuer espera respuesta? ¿Vía reply queue, webhook, polling?
- Retry policy: ¿cuántos reintentos? ¿Con qué backoff? ¿Cuándo va a dead-letter?
- Ordering: ¿los comandos para la misma entidad deben procesarse en orden?
8. Ejemplo Arquitectónico Detallado¶
Dominio: Banca — Transferencia Interbancaria (Wire Transfer)¶
Contexto del Negocio¶
Un banco digital opera una plataforma de banca online donde los clientes pueden iniciar transferencias interbancarias (wire transfers) a otros bancos. Cada transferencia involucra múltiples pasos: validación de fondos, verificación de compliance (anti-money laundering), débito de la cuenta origen, envío al sistema de pagos interbancarios (SWIFT/SEPA), y confirmación al cliente.
El volumen diario es de 200,000 transferencias, con picos de 50,000 por hora durante horarios laborales. Las transferencias internacionales tienen requisitos de compliance más estrictos que las domésticas. El sistema debe garantizar que ninguna transferencia se pierda y que cada una se procese exactamente una vez.
Necesidad de Integración¶
El servicio de banca online (frontend/API) necesita delegar la ejecución de la transferencia al servicio de procesamiento de pagos. La ejecución es compleja (múltiples validaciones, interacción con sistemas externos) y puede tardar segundos o minutos. El servicio de banca online no puede bloquear esperando el resultado — debe aceptar la petición del cliente, encolarla como comando y notificar al cliente cuando se complete.
Sistemas Involucrados¶
- Banking API Service: recibe la petición del cliente y construye el comando.
- Payment Command Queue: canal point-to-point que almacena los comandos de transferencia.
- Payment Processing Service: consume y ejecuta los comandos de transferencia.
- AML Compliance Service: valida las transferencias contra reglas anti-lavado.
- Core Banking System: ejecuta los débitos y créditos.
- SWIFT Gateway: envía instrucciones de pago al sistema interbancario.
- Notification Service: notifica al cliente del resultado.
- Payment Response Queue: canal por donde se notifica el resultado al Banking API.
- Dead Letter Queue: almacena comandos que fallaron después de todos los reintentos.
Restricciones Técnicas¶
- Cada transferencia debe procesarse exactamente una vez (no puede duplicarse un débito).
- Las transferencias internacionales > 10,000 EUR requieren verificación AML síncrona antes de procesarse.
- La latencia aceptable entre la petición del cliente y la confirmación de aceptación es de 2 segundos.
- La latencia aceptable entre la aceptación y la ejecución es de 30 minutos (para transferencias domésticas) o 4 horas (para internacionales).
- El sistema debe soportar 50,000 comandos/hora en pico.
- Los comandos para la misma cuenta origen deben procesarse en orden para evitar condiciones de carrera en el saldo.
Diseño del Command Message¶
{
"command_type": "InitiateWireTransfer",
"command_id": "cmd-2026-04-07-a8f3c912",
"correlation_id": "txn-2026-1847291",
"timestamp": "2026-04-07T14:32:15Z",
"return_address": "payment.responses",
"payload": {
"transfer_id": "TRF-2026-00482917",
"source_account": "ES91 2100 0418 4502 0005 1332",
"destination_account": "DE89 3704 0044 0532 0130 00",
"destination_bank_bic": "COBADEFFXXX",
"amount": {
"value": 15000.00,
"currency": "EUR"
},
"transfer_type": "SEPA_INTERNATIONAL",
"purpose": "Invoice payment INV-2026-3847",
"requested_execution_date": "2026-04-07",
"customer_id": "CUST-00291847",
"priority": "NORMAL"
},
"metadata": {
"source_service": "banking-api",
"source_version": "3.2.1",
"idempotency_key": "idem-TRF-2026-00482917",
"schema_version": "2.0"
}
}
Decisiones Arquitectónicas¶
-
Self-contained command: el comando incluye todos los datos necesarios para ejecutar la transferencia. El Payment Processing Service no necesita consultar al Banking API para obtener datos adicionales.
-
Idempotency key explícita: el
idempotency_keypermite que el handler detecte y descarte reintentos duplicados sin re-ejecutar la transferencia. -
Return address: el
return_addressindica el canal donde el handler debe publicar el resultado, implementando Request-Reply asíncrono. -
Partition key por cuenta origen: los mensajes se particionan por
source_account, garantizando que todos los comandos para la misma cuenta se procesen en orden secuencial. -
Separación de validación estructural y de negocio: el Banking API valida formato (IBAN válido, amount > 0), y el Payment Processing Service valida negocio (fondos suficientes, cuenta activa, límites de transferencia).
Riesgos y Mitigaciones¶
| Riesgo | Mitigación |
|---|---|
| Comando duplicado ejecuta doble débito | Idempotency key + tabla de comandos procesados |
| Fallo entre débito y envío SWIFT | Saga pattern con compensación (re-crédito) |
| Queue pierde el comando | Queue durable con replicación y acks=all |
| AML check tarda demasiado | Timeout configurable + fallback a revisión manual |
| Pico de volumen satura el handler | Competing Consumers (múltiples instancias del handler) |
| Comando envenenado bloquea la queue | Max retries + dead-letter con alerta |
9. Desarrollo Paso a Paso del Ejemplo¶
Paso 1: Recepción de la Petición del Cliente¶
Un cliente del banco inicia una transferencia de 15,000 EUR desde su cuenta española a una cuenta alemana a través de la app móvil. La petición llega al Banking API Service como un HTTP POST:
POST /api/v2/transfers
Content-Type: application/json
Authorization: Bearer eyJ...
Idempotency-Key: idem-TRF-2026-00482917
El Banking API Service: 1. Autentica y autoriza al cliente. 2. Valida el formato de los datos (IBAN válido, amount > 0, currency soportada). 3. Genera un transfer_id único. 4. Construye el Command Message InitiateWireTransfer. 5. Envía el comando a la queue payment.commands.wire-transfer. 6. Responde al cliente con HTTP 202 Accepted y el transfer_id para seguimiento.
La respuesta inmediata al cliente tarda < 500ms. El procesamiento real ocurrirá asincrónicamente.
Paso 2: Encolamiento del Comando¶
El comando se publica en la queue payment.commands.wire-transfer con las siguientes propiedades:
- Partition key: hash de
source_account(ES91...) → partición 17. - Durabilidad: el broker persiste el mensaje en disco y lo replica a 2 nodos adicionales.
- TTL: 24 horas (si no se procesa en 24h, va a dead-letter).
- Priority: NORMAL (las transferencias urgentes usan prioridad HIGH).
El broker confirma la recepción (ack). El Banking API registra en su base de datos local que el comando fue encolado exitosamente (status: QUEUED).
Paso 3: Consumo y Validación del Comando¶
El Payment Processing Service (consumer group: cg-payment-processor, 8 instancias) consume el comando de la partición 17. La instancia asignada a esa partición:
- Deserializa el comando y verifica que el
schema_versiones compatible. - Verifica idempotencia: consulta la tabla
processed_commandsbuscandoidempotency_key = idem-TRF-2026-00482917. No existe — es un comando nuevo. - Valida precondiciones de negocio:
- Cuenta origen ES91... existe y está activa: OK.
- Saldo disponible >= 15,000 EUR: OK (saldo: 47,320.15 EUR).
- Límite de transferencia diaria no excedido: OK (acumulado hoy: 3,000 EUR, límite: 50,000 EUR).
- Transferencia internacional > 10,000 EUR: requiere verificación AML.
Paso 4: Verificación de Compliance¶
El Payment Processing Service invoca síncronamente al AML Compliance Service (porque la verificación AML es un requisito previo a cualquier movimiento de fondos):
- Envía los datos de la transferencia al AML Service.
- El AML Service verifica contra listas de sanciones, patrones sospechosos y reglas de jurisdicción.
- Resultado:
APPROVED(la transferencia no presenta indicadores de lavado).
Si el resultado fuera REJECTED o MANUAL_REVIEW, el comando se rechazaría y se enviaría una respuesta de rechazo al canal payment.responses.
Paso 5: Ejecución de la Transferencia¶
Con la validación y compliance aprobadas, el Payment Processing Service ejecuta la transferencia:
- Débito: invoca al Core Banking System para debitar 15,000 EUR de la cuenta ES91... Se registra un hold (reserva) que se convertirá en débito definitivo cuando SWIFT confirme.
- Instrucción SWIFT: envía la instrucción de pago al SWIFT Gateway con los datos de la cuenta destino (DE89...), banco destino (COBADEFFXXX), importe y referencia.
- Registro: guarda el
command_iden la tablaprocessed_commandspara idempotencia futura. - Acknowledgment: confirma el procesamiento del mensaje (commit offset / ack).
Paso 6: Respuesta al Emisor¶
El Payment Processing Service publica un mensaje de respuesta en el canal payment.responses (el return_address especificado en el comando):
{
"response_type": "WireTransferInitiated",
"correlation_id": "txn-2026-1847291",
"transfer_id": "TRF-2026-00482917",
"status": "PROCESSING",
"swift_reference": "SWIFT-2026-04-07-3829",
"estimated_completion": "2026-04-07T18:00:00Z"
}
El Banking API Service consume esta respuesta, actualiza el estado de la transferencia en su base de datos (status: PROCESSING) y notifica al cliente vía push notification.
Paso 7: Manejo de Fallos¶
Si el débito falla (por ejemplo, fondos insuficientes detectados en el momento exacto del débito por otra transacción concurrente):
- El handler no confirma el mensaje (nack o no commit offset).
- El broker re-entrega el comando después de un backoff.
- En el reintento, la validación de saldo falla.
- El handler envía una respuesta de rechazo al canal
payment.responses. - El Banking API notifica al cliente que la transferencia fue rechazada por fondos insuficientes.
Si el handler crashea durante el procesamiento, la tabla processed_commands determina si el débito ya ocurrió (y entonces solo falta enviar la instrucción SWIFT) o si no ocurrió (y se reintenta desde el inicio).
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 RabbitMQ
from diagrams.onprem.compute import Server
from diagrams.onprem.database import PostgreSQL
from diagrams.onprem.network import Nginx
from diagrams.onprem.monitoring import Grafana
from diagrams.programming.framework import React
with Diagram("Command Message - Wire Transfer Processing", show=False, direction="LR"):
with Cluster("Client Layer"):
client = React("Mobile App")
with Cluster("API Layer"):
api = Nginx("Banking API\nGateway")
banking_api = Server("Banking API\nService")
with Cluster("Command Infrastructure"):
cmd_queue = RabbitMQ("payment.commands\n.wire-transfer\n(P2P Queue)")
resp_queue = RabbitMQ("payment.responses\n(Reply Queue)")
dlq = RabbitMQ("payment.commands\n.dead-letter\n(DLQ)")
with Cluster("Command Handlers"):
handler1 = Server("Payment\nProcessor #1")
handler2 = Server("Payment\nProcessor #2")
handler3 = Server("Payment\nProcessor #3")
with Cluster("Backend Services"):
aml = Server("AML Compliance\nService")
core = Server("Core Banking\nSystem")
swift = Server("SWIFT\nGateway")
with Cluster("Data Stores"):
db_api = PostgreSQL("API DB\n(Transfer Status)")
db_proc = PostgreSQL("Processor DB\n(Idempotency)")
monitoring = Grafana("Queue\nMonitoring")
# Flow
client >> api >> banking_api
banking_api >> Edge(label="1. Send Command") >> cmd_queue
banking_api >> db_api
cmd_queue >> Edge(label="2. Consume") >> handler1
cmd_queue >> handler2
cmd_queue >> handler3
handler1 >> Edge(label="3. Validate") >> aml
handler1 >> Edge(label="4. Debit") >> core
handler1 >> Edge(label="5. SWIFT") >> swift
handler1 >> db_proc
handler1 >> Edge(label="6. Response", style="dashed") >> resp_queue
cmd_queue >> Edge(label="DLQ", style="dotted", color="red") >> dlq
resp_queue >> Edge(label="7. Result", style="dashed") >> banking_api
cmd_queue >> Edge(style="dotted") >> monitoring
dlq >> Edge(style="dotted") >> monitoring
from diagrams import Diagram, Cluster, Edge
from diagrams.aws.compute import Lambda, ECS
from diagrams.aws.database import Dynamodb, Aurora
from diagrams.aws.integration import SQS, StepFunctions
from diagrams.aws.management import Cloudwatch
from diagrams.aws.network import APIGateway
with Diagram("Command Message - Wire Transfer Processing (AWS)", show=False, direction="LR"):
with Cluster("Client Layer"):
client = Lambda("Mobile App\nBackend")
with Cluster("API Layer"):
api = APIGateway("API Gateway\n(REST)")
banking_api = Lambda("Banking API\nService")
with Cluster("Command Infrastructure"):
cmd_queue = SQS("payment.commands\n.wire-transfer\n(SQS Queue)")
resp_queue = SQS("payment.responses\n(Reply Queue)")
dlq = SQS("payment.commands\n.dead-letter\n(DLQ)")
with Cluster("Command Orchestration"):
orchestrator = StepFunctions("Wire Transfer\nStep Functions\n(State Machine)")
with Cluster("Backend Services"):
aml = ECS("AML Compliance\nService")
core = ECS("Core Banking\nSystem")
swift = ECS("SWIFT\nGateway")
with Cluster("Data Stores"):
db_api = Aurora("Aurora DB\n(Transfer Status)")
db_proc = Dynamodb("Dynamodb\n(Idempotency)")
monitoring = Cloudwatch("Queue\nMonitoring")
# Flow
client >> api >> banking_api
banking_api >> Edge(label="1. Send Command") >> cmd_queue
banking_api >> db_api
cmd_queue >> Edge(label="2. Trigger") >> orchestrator
orchestrator >> db_proc
orchestrator >> Edge(label="3. Validate") >> aml
orchestrator >> Edge(label="4. Debit") >> core
orchestrator >> Edge(label="5. SWIFT") >> swift
orchestrator >> Edge(label="6. Response", style="dashed") >> resp_queue
cmd_queue >> Edge(label="DLQ", style="dotted", color="red") >> dlq
resp_queue >> Edge(label="7. Result", style="dashed") >> banking_api
cmd_queue >> Edge(style="dotted") >> monitoring
dlq >> Edge(style="dotted") >> monitoring
from diagrams import Diagram, Cluster, Edge
from diagrams.programming.framework import React
from diagrams.azure.compute import FunctionApps
from diagrams.azure.database import SQLServers
from diagrams.azure.devops import ApplicationInsights
from diagrams.azure.integration import ServiceBus, APIManagement
with Diagram("Command Message - Wire Transfer Processing (Azure)", show=False, direction="LR"):
with Cluster("Client Layer"):
client = React("Mobile App")
with Cluster("API Layer"):
api = APIManagement("API Management\nGateway")
banking_api = FunctionApps("Banking API\nService")
with Cluster("Service Bus Command Infrastructure"):
cmd_queue = ServiceBus("payment.commands\n.wire-transfer\n(Queue)")
resp_queue = ServiceBus("payment.responses\n(Reply Queue)")
dlq = ServiceBus("payment.commands\n.dead-letter\n(DLQ)")
with Cluster("Command Handlers"):
handler1 = FunctionApps("Payment\nProcessor #1")
handler2 = FunctionApps("Payment\nProcessor #2")
handler3 = FunctionApps("Payment\nProcessor #3")
with Cluster("Backend Services"):
aml = FunctionApps("AML Compliance\nService")
core = FunctionApps("Core Banking\nSystem")
swift = FunctionApps("SWIFT\nGateway")
with Cluster("Data Stores"):
db_api = SQLServers("Azure SQL\n(Transfer Status)")
db_proc = SQLServers("Azure SQL\n(Idempotency)")
monitoring = ApplicationInsights("Application\nInsights")
# Flow
client >> api >> banking_api
banking_api >> Edge(label="1. Send Command") >> cmd_queue
banking_api >> db_api
cmd_queue >> Edge(label="2. Consume") >> handler1
cmd_queue >> handler2
cmd_queue >> handler3
handler1 >> Edge(label="3. Validate") >> aml
handler1 >> Edge(label="4. Debit") >> core
handler1 >> Edge(label="5. SWIFT") >> swift
handler1 >> db_proc
handler1 >> Edge(label="6. Response", style="dashed") >> resp_queue
cmd_queue >> Edge(label="DLQ", style="dotted", color="red") >> dlq
resp_queue >> Edge(label="7. Result", style="dashed") >> banking_api
cmd_queue >> Edge(style="dotted") >> monitoring
dlq >> Edge(style="dotted") >> monitoring
Explicación del Diagrama¶
El diagrama muestra el flujo completo de un Command Message en el sistema de transferencias bancarias:
- El cliente envía la petición al Banking API a través del gateway.
- El Banking API construye el Command Message y lo envía a la queue payment.commands.wire-transfer (canal point-to-point).
- Tres instancias del Payment Processor compiten por los comandos (Competing Consumers pattern), proporcionando paralelismo y resiliencia.
- El processor seleccionado valida con AML Compliance, debita en Core Banking y envía la instrucción a SWIFT.
- El resultado se publica en la reply queue y llega de vuelta al Banking API.
- Los comandos que fallan todos los reintentos van a la Dead Letter Queue.
- Grafana monitorea el estado de las queues (profundidad, error rate, processing time).
Correspondencia Patrón ↔ Diagrama¶
| Concepto del Patrón | Componente del Diagrama |
|---|---|
| Command Issuer | Banking API Service |
| Command Message | JSON message en payment.commands.wire-transfer |
| Command Queue (P2P) | payment.commands.wire-transfer |
| Command Handler | Payment Processor #1, #2, #3 (Competing Consumers) |
| Return Address | payment.responses queue |
| Correlation Identifier | correlation_id en el mensaje |
| Dead Letter Channel | payment.commands.dead-letter |
| Idempotency Store | Processor DB (tabla processed_commands) |
11. Beneficios¶
Impacto Técnico¶
- Desacoplamiento temporal: el Banking API acepta la transferencia en milisegundos y la encola. El procesamiento puede tardar minutos sin afectar la experiencia del cliente.
- Resiliencia: si el Payment Processing Service está caído, los comandos se acumulan en la queue y se procesan cuando el servicio se recupere. Ninguna transferencia se pierde.
- Escalabilidad horizontal: añadir más instancias del handler (Competing Consumers) incrementa el throughput proporcionalmente, sin modificar el productor.
- Garantía de ejecución: la combinación de queue durable + idempotencia + dead-letter garantiza que cada comando se procesará exactamente una vez o se escalará para intervención manual.
- Priorización: los comandos pueden encolarse con diferentes prioridades. Transferencias urgentes se procesan antes que las normales.
Impacto Organizacional¶
- Autonomía de equipos: el equipo del Banking API y el equipo de Payment Processing trabajan de forma independiente. El contrato es la estructura del Command Message, no el código compartido.
- Claridad semántica: el nombre del comando (
InitiateWireTransfer) documenta inequívocamente la intención. Cualquier desarrollador que vea el mensaje entiende qué debe ocurrir. - Evolución independiente: el handler puede cambiar su implementación interna (nuevo motor de compliance, nuevo gateway SWIFT) sin afectar al issuer, siempre que respete el contrato del comando.
Impacto Operacional¶
- Visibilidad: la profundidad de la queue de comandos es un indicador directo de la carga pendiente. Un aumento inusual indica problemas en el handler o picos de demanda.
- Retry automático: los comandos que fallan se reintentan automáticamente con backoff, sin intervención humana.
- Debugging: un comando en la dead-letter queue puede inspeccionarse para entender exactamente qué falló, con todos los datos de entrada intactos.
- Throttling natural: la queue actúa como buffer entre la demanda (peticiones de clientes) y la capacidad (instancias del handler), evitando sobrecarga.
Beneficios de Mantenibilidad y Evolución¶
- Migración del handler: el Payment Processing Service puede reescribirse completamente (nuevo lenguaje, nueva arquitectura) sin afectar al Banking API, siempre que consuma los mismos comandos del mismo canal.
- Testing: los comandos pueden inyectarse directamente en la queue para testing end-to-end sin necesidad de simular la interfaz del cliente.
- Audit trail: cada comando en la queue es un registro auditable de qué se solicitó, cuándo y por quién.
12. Desventajas y Riesgos¶
Complejidad Añadida¶
- Latencia de procesamiento: a diferencia de una llamada síncrona, el resultado no está disponible inmediatamente. El cliente debe aceptar un modelo de "petición aceptada, resultado pendiente".
- Infraestructura de messaging: operar una queue durable con alta disponibilidad (RabbitMQ cluster, Azure Service Bus Premium) requiere expertise y costo operacional.
- Complejidad de idempotencia: implementar idempotencia correctamente (especialmente en flujos multi-step como la transferencia) no es trivial y requiere almacenamiento adicional.
- Gestión de respuestas: si el issuer necesita el resultado, debe implementar Request-Reply con Correlation Identifier, añadiendo complejidad.
Riesgos de Mal Uso¶
- Command as event: usar Command Message cuando la semántica debería ser un evento. Si el productor no tiene expectativa de que un servicio específico haga algo, sino que está notificando un hecho, debe usar Event Message.
- Command to many: enviar un command a un canal pub-sub, haciendo que múltiples handlers lo ejecuten simultáneamente. Los comandos deben ir a canales point-to-point.
- Comando sin idempotencia: en un sistema at-least-once, no implementar idempotencia en el handler produce ejecuciones duplicadas con consecuencias potencialmente graves (doble débito).
- Comando demasiado genérico: un comando
ProcessActioncon un campoaction_typeque determina qué hacer es un anti-pattern. Cada acción debe tener su propio tipo de comando con su propio schema.
Sobreingeniería¶
- Command bus innecesario: implementar un command bus completo (con middlewares, pipelines, interceptors) para un sistema simple con 3 tipos de comando es sobreingeniería. La complejidad del framework debe justificarse por la complejidad del dominio.
- Sobre-granularidad: crear un tipo de comando diferente para cada variación menor de una acción (TransferEUR, TransferUSD, TransferGBP en lugar de Transfer con un campo currency) produce una explosión de tipos sin beneficio.
Costos de Operación¶
- Queue monitoring: cada queue de comandos requiere monitoreo de profundidad, error rate, processing time y dead-letter rate.
- Dead letter management: los comandos en dead-letter necesitan procesos operacionales para inspección, remediación y re-enqueue.
- Schema evolution: evolucionar el schema de un comando en producción (añadir campos obligatorios, cambiar tipos) requiere coordinación entre issuer y handler y estrategia de compatibilidad.
Anti-Patterns Relacionados¶
- God Command: un único tipo de comando genérico que contiene todos los posibles datos y un campo
actionque determina qué hacer. Esto elimina el type safety y dificulta la validación. - Fire-and-Forget sin DLQ: enviar comandos sin dead-letter queue ni monitoreo. Los comandos que fallan desaparecen silenciosamente.
- Synchronous Command: encolar un comando y luego hacer polling síncrono de la respuesta, eliminando todos los beneficios del messaging asíncrono.
13. Relación con Otros Patrones¶
Patrones Complementarios¶
- Event Message (este capítulo): frecuentemente, después de ejecutar un comando, el handler emite un evento notificando que la acción ocurrió. El comando
InitiateWireTransferproduce el eventoWireTransferInitiated. Esta relación command→event es fundamental en CQRS y event sourcing. - Document Message (este capítulo): un comando puede contener un documento embebido (los datos de la transferencia), pero la semántica del mensaje es imperativa, no informativa. La diferencia está en la intención.
- Request-Reply (este capítulo): cuando el issuer necesita el resultado del comando, Request-Reply proporciona el patrón de interacción bidireccional.
- Return Address (este capítulo): especifica en el comando dónde enviar la respuesta.
- Correlation Identifier (este capítulo): vincula la respuesta con el comando original.
Patrones que Suelen Aparecer Antes o Después¶
- Antes: Message Channel — el canal point-to-point debe existir antes de enviar comandos.
- Después: Event Message — el handler normalmente emite un evento después de ejecutar el comando.
- Complementario: Competing Consumers — múltiples instancias del handler procesan comandos en paralelo.
- Complementario: Dead Letter Channel — destino de comandos que no pueden procesarse.
Combinaciones Comunes¶
- Command Message + Saga: un orquestador envía una secuencia de comandos a diferentes servicios. Si un comando falla, el orquestador envía comandos de compensación a los servicios previos.
- Command Message + Content-Based Router: un router examina el tipo de comando y lo dirige al handler apropiado.
- Command Message + Message Expiration: comandos con TTL para evitar ejecutar acciones obsoletas.
Diferencias con Patrones Similares¶
- vs. Event Message: el evento informa de algo que ya ocurrió; el comando instruye a que algo ocurra. El evento puede tener múltiples suscriptores; el comando tiene un único handler.
- vs. Document Message: el documento transfiere datos sin prescribir acción; el comando prescribe una acción específica.
- vs. Remote Procedure Invocation: RPI es síncrono y acoplado temporalmente; Command Message es asíncrono y desacoplado.
Encaje en un Flujo Mayor de Integración¶
Command Message es el mecanismo primario de comunicación en el lado de escritura de una arquitectura CQRS. En una saga distribuida, los comandos son los mensajes que coordinan las acciones de los participantes. En una arquitectura de microservicios, los comandos complementan a los eventos: los eventos desacoplan la notificación de hechos, y los comandos habilitan la delegación explícita de acciones.
14. Relevancia Actual del Patrón¶
Evaluación: Relevancia Alta¶
Argumentación¶
Command Message es más relevante que nunca en las arquitecturas modernas:
- CQRS (Command Query Responsibility Segregation): la "C" de CQRS son literalmente Command Messages. Frameworks como MediatR (.NET), Axon (Java) y Commanded (Elixir) implementan command buses que reciben y despachan Command Messages.
- Task queues: Celery (Python), Sidekiq (Ruby), Bull (Node.js), Hangfire (.NET) son todas implementaciones de Command Message. Cada "task" o "job" es un comando que se encola para ejecución asíncrona.
- Saga orchestration: frameworks como Temporal, Apache Camel, MassTransit y NServiceBus implementan sagas donde el orquestador envía Command Messages a los participantes.
- Serverless: en AWS, un mensaje en SQS que dispara una Lambda es un Command Message. El mensaje dice "ejecuta esta función con estos datos".
- Kubernetes Jobs: un Job de Kubernetes iniciado por un mensaje es la ejecución de un Command Message.
Cómo Se Implementa Hoy¶
| Plataforma / Framework | Implementación de Command Message |
|---|---|
| MediatR (.NET) | IRequest<TResponse> + IRequestHandler<TRequest, TResponse> |
| Axon Framework (Java) | @CommandHandler + Command Bus |
| Celery (Python) | @task decorator, task.delay() para envío |
| Sidekiq (Ruby) | perform_async + Worker class |
| Bull / BullMQ (Node.js) | queue.add() + Worker processor |
| AWS SQS + Lambda | SQS message → Lambda invocation |
| Azure Service Bus + Functions | Queue message → Azure Function trigger |
| RabbitMQ | Queue con message acknowledgment |
| Temporal | Workflow execute_activity() commands |
| MassTransit (.NET) | ISendEndpoint.Send<TCommand>() |
Qué Parte Sigue Siendo Esencial¶
- La semántica imperativa: la distinción entre "haz esto" (command) y "esto ocurrió" (event) sigue siendo la decisión de diseño más importante en messaging.
- La entrega a un único handler: el principio de single responsibility del comando (un handler por tipo de comando) es fundamental para mantener la claridad del sistema.
- La idempotencia: en sistemas at-least-once, la idempotencia del handler es un requisito no negociable.
- La queue como buffer: la capacidad de absorber picos de carga encolando comandos sigue siendo una de las principales ventajas del patrón.
15. Implementación en Arquitecturas Modernas¶
Python con Celery (Task Queue)¶
# Command definition (task)
@app.task(
bind=True,
max_retries=3,
default_retry_delay=60,
acks_late=True,
reject_on_worker_lost=True
)
def initiate_wire_transfer(self, command: dict):
"""Command Handler for wire transfer initiation."""
idempotency_key = command["metadata"]["idempotency_key"]
# Idempotency check
if TransferRecord.objects.filter(idempotency_key=idempotency_key).exists():
logger.info(f"Duplicate command detected: {idempotency_key}")
return {"status": "ALREADY_PROCESSED"}
try:
# Validate preconditions
validate_transfer_command(command["payload"])
# Execute
result = execute_wire_transfer(command["payload"])
# Record for idempotency
TransferRecord.objects.create(
idempotency_key=idempotency_key,
command_id=command["command_id"],
status="COMPLETED"
)
return result
except InsufficientFundsError as e:
TransferRecord.objects.create(
idempotency_key=idempotency_key,
command_id=command["command_id"],
status="REJECTED"
)
return {"status": "REJECTED", "reason": str(e)}
except TransientError as e:
raise self.retry(exc=e)
.NET con MediatR (CQRS Command)¶
// Command definition
public record InitiateWireTransferCommand(
string TransferId,
string SourceAccount,
string DestinationAccount,
decimal Amount,
string Currency
) : IRequest<TransferResult>;
// Command handler
public class InitiateWireTransferHandler
: IRequestHandler<InitiateWireTransferCommand, TransferResult>
{
public async Task<TransferResult> Handle(
InitiateWireTransferCommand command,
CancellationToken ct)
{
// Validate, execute, return result
}
}
// Command issuer (via queue consumer)
var command = JsonSerializer.Deserialize<InitiateWireTransferCommand>(message.Body);
var result = await mediator.Send(command, ct);
Azure Service Bus (Cloud-Native)¶
Queue: payment-commands-wire-transfer
Max Delivery Count: 5
Lock Duration: 5 minutes
Dead-Letter: payment-commands-wire-transfer/$deadletterqueue
Sessions: Enabled (partition by source_account)
Duplicate Detection: Enabled (10 min window)
Azure Service Bus Sessions garantizan que todos los comandos con la misma session ID (cuenta origen) se procesan secuencialmente por la misma instancia del handler, resolviendo el problema de orden por entidad.
Apache Kafka¶
Topic: payment.commands.wire-transfer
Partitions: 16 (por hash de source_account)
Replication Factor: 3
Cleanup Policy: delete
Retention: 24 hours
Min ISR: 2
Kafka como queue de comandos requiere un único consumer group. La partition key por source_account garantiza orden por cuenta. La retención de 24h permite reprocesamiento si es necesario.
Temporal (Saga Orchestration)¶
@workflow.defn
class WireTransferSaga:
@workflow.run
async def run(self, transfer_request: TransferRequest):
# Each execute_activity sends a Command Message
aml_result = await workflow.execute_activity(
check_aml_compliance,
transfer_request,
start_to_close_timeout=timedelta(minutes=5)
)
debit_result = await workflow.execute_activity(
debit_account,
transfer_request,
start_to_close_timeout=timedelta(minutes=2),
retry_policy=RetryPolicy(maximum_attempts=3)
)
swift_result = await workflow.execute_activity(
send_swift_instruction,
transfer_request,
start_to_close_timeout=timedelta(minutes=10)
)
En Temporal, cada execute_activity es conceptualmente un Command Message enviado al worker que ejecuta la actividad. Temporal gestiona la queue, el retry, la idempotencia y la compensación.
16. Consideraciones de Gobierno y Operación¶
Observabilidad¶
- Métricas por queue de comandos: commands-enqueued/sec, commands-processed/sec, commands-failed/sec, queue depth, average processing time, dead-letter count.
- Distributed tracing: propagar
correlation_idy trace context (W3C Trace Context) en los headers del comando para trazabilidad end-to-end desde la petición del cliente hasta la ejecución del comando. - Structured logging: cada paso del procesamiento del comando (received, validated, executing, completed, failed, retrying, dead-lettered) debe generar un log estructurado con command_id, command_type, correlation_id, timestamp y outcome.
Monitoreo¶
- Queue depth: la métrica más importante. Un crecimiento sostenido indica que los handlers no pueden mantener el ritmo.
- Processing time p95/p99: el tiempo que tarda un comando desde que se encola hasta que se completa. Incrementos indican degradación del handler o de los sistemas backend.
- Dead-letter rate: porcentaje de comandos que terminan en DLQ. Un incremento abrupto indica un problema sistémico.
- Retry rate: tasa de reintentos. Altas tasas de retry sin incremento de dead-letter sugieren problemas transitorios recurrentes.
Versionado¶
- Schema Registry: versionar el schema de cada tipo de comando con compatibilidad BACKWARD (los handlers nuevos aceptan comandos viejos, los handlers viejos pueden ignorar campos nuevos de forma segura).
- Command versioning: incluir
schema_versionen metadata. Si se necesita un cambio incompatible, crear un nuevo tipo de comando (InitiateWireTransferV2) y deprecar el anterior gradualmente.
Seguridad¶
- Authentication: el issuer debe autenticarse ante el broker para enviar comandos.
- Authorization: solo servicios autorizados pueden enviar comandos a cada queue. El Payment Processing Service no debería poder enviar comandos a sí mismo.
- Payload encryption: para comandos con datos sensibles (números de cuenta, montos), considerar cifrado del payload además de TLS en tránsito.
- Input validation: el handler debe validar exhaustivamente el payload del comando. Un comando malformado o malicioso debe rechazarse sin ejecutar ninguna acción.
Manejo de Errores y Dead-Lettering¶
- Retry policy: definir max_retries y backoff strategy por tipo de comando. Errores transitorios (timeout de red, servicio temporalmente no disponible) merecen retry; errores de negocio (fondos insuficientes) no.
- Dead-letter processing: implementar un proceso (manual o automatizado) para inspeccionar, corregir y re-encolar comandos de la DLQ.
- Alertas: una alerta inmediata cuando un comando llega a DLQ en un dominio crítico (pagos, transferencias).
Idempotencia¶
- Idempotency key: cada comando debe incluir una clave de idempotencia (puede ser el command_id o un campo explícito).
- Idempotency store: una tabla o cache donde el handler registra qué comandos ha procesado. Antes de ejecutar, verifica si la clave ya existe.
- Ventana de deduplicación: la idempotency store debe retener claves durante un período suficiente para cubrir todos los posibles reintentos (típicamente 24-72 horas).
Auditoría¶
- Registrar cada comando recibido con su payload completo, timestamp, issuer y resultado.
- Mantener un log de comandos rechazados con la razón del rechazo.
- Los registros de auditoría de comandos financieros deben ser inmutables y retención regulatoria (5-7 años).
Performance¶
- Batch acknowledgment: confirmar múltiples comandos en batch para amortizar overhead.
- Prefetch: configurar prefetch count para que el handler tenga comandos listos cuando termine de procesar el actual.
- Connection pooling: reutilizar conexiones al broker y a los sistemas backend.
Escalabilidad¶
- Horizontal: añadir instancias del handler (Competing Consumers) para escalar throughput.
- Partitioning: más particiones en Kafka o más sessions en Service Bus permiten mayor paralelismo.
- Auto-scaling: escalar el número de handlers basándose en la profundidad de la queue (KEDA en Kubernetes, Lambda concurrency en AWS).
17. Errores Comunes¶
Confundir Commands con Events¶
El error más frecuente y más dañino. Si un servicio emite un mensaje TransferFunds a un topic pub-sub y múltiples servicios lo procesan, la transferencia se ejecuta múltiples veces. Los comandos van a queues point-to-point con un único handler. Si la intención es notificar, es un evento, no un comando.
No Implementar Idempotencia¶
En un sistema at-least-once (que es el default en la mayoría de brokers), el handler recibirá el mismo comando al menos una vez, potencialmente más. Sin idempotencia, una transferencia puede ejecutarse dos veces. La idempotencia no es un "nice to have" — es un requisito de correctitud.
Comandos Sin Datos Suficientes¶
Un comando que dice "procesa la transferencia TRF-123" sin incluir los datos de la transferencia obliga al handler a consultar otro servicio para obtenerlos. Esto introduce dependencias runtime y puntos de fallo adicionales. Los comandos deben ser self-contained siempre que sea práctico.
Ignorar el Rechazo de Comandos¶
A diferencia de los eventos (que son hechos consumados), los comandos pueden rechazarse por precondiciones no cumplidas. Si el issuer no tiene un mecanismo para recibir y manejar rechazos, las acciones fallidas pasan desapercibidas.
Queue Sin Dead-Letter¶
Comandos que fallan repetidamente sin ir a dead-letter bloquean la queue (si el reintento es infinito) o se pierden silenciosamente (si se descartan). Toda queue de comandos debe tener una DLQ configurada y monitoreada.
Acoplamiento Excesivo en el Nombre del Comando¶
Nombrar un comando con detalles de implementación (InsertTransferRowInOracleTable) en lugar de intención de negocio (InitiateWireTransfer) acopla el issuer a los detalles internos del handler. Los comandos deben expresar intención de negocio.
Usar Commands para Notificación¶
Si el objetivo es informar a múltiples servicios de que algo ocurrió, usar Command Message es incorrecto. Enviar NotifyInventory, NotifyShipping, NotifyBilling cuando lo que ocurrió es un hecho (OrderPlaced) obliga al productor a conocer a todos los consumidores y a mantenerse actualizado cuando se añaden nuevos. Esto es responsabilidad de Event Message.
18. Conclusión Técnica¶
Command Message es el patrón que establece cómo modelar intenciones de acción como mensajes en una arquitectura distribuida. Su semántica imperativa — "haz esto" — lo diferencia fundamentalmente de Event Message ("esto ocurrió") y Document Message ("aquí están los datos"). Esta distinción no es académica; determina el tipo de canal (point-to-point vs. pub-sub), el número de consumidores (uno vs. muchos), la posibilidad de rechazo y la estrategia de manejo de errores.
Cuándo aporta valor: siempre que un servicio necesite delegar una acción específica a otro servicio de forma asíncrona y con garantía de ejecución. Los escenarios más comunes son CQRS command handling, saga orchestration, task queues y asynchronous API processing. En todos estos casos, Command Message proporciona desacoplamiento temporal, resiliencia ante fallos del handler, escalabilidad horizontal y visibilidad operacional.
Cuándo evita problemas importantes: la alternativa a Command Message en la mayoría de los casos es la invocación síncrona directa (REST/gRPC). Command Message evita los problemas clásicos de la comunicación síncrona: acoplamiento temporal, fallos en cascada, imposibilidad de absorber picos de carga y pérdida de acciones durante downtime del handler.
Cuándo no conviene adoptarlo: cuando la latencia del procesamiento asíncrono no es aceptable (el usuario necesita una respuesta instantánea), cuando la complejidad de la infraestructura de messaging no se justifica (un sistema monolítico con 3 operaciones de escritura) o cuando la semántica no es realmente imperativa (se quiere notificar, no ordenar).
Recomendación para arquitectos: trate cada Command Message con la seriedad de un contrato de API. Defina un schema explícito para cada tipo de comando, implemente idempotencia en cada handler sin excepción, configure dead-letter queues con monitoreo y alertas, y mantenga la disciplina de nombrar comandos en imperativo (TransferFunds, CancelOrder) para que la semántica sea inconfundible. La claridad en la distinción command vs. event es, con frecuencia, lo que separa una arquitectura event-driven bien diseñada de una maraña de mensajes ambiguos.


