Saltar a contenido

Request-Reply

1. Nombre del Patrón

  • Nombre oficial: Request-Reply
  • Categoría: Message Construction (Construcción de Mensajes)
  • Traducción contextual: Petición-Respuesta

2. Resumen Ejecutivo

Request-Reply es el patrón que permite establecer una conversación bidireccional sobre un sistema de mensajería asíncrono. El productor envía un mensaje de petición (request) a un canal y espera recibir un mensaje de respuesta (reply) en un segundo canal, logrando así una semántica de invocación síncrona sobre una infraestructura fundamentalmente asíncrona.

El problema que resuelve es central en toda arquitectura distribuida: ¿cómo puede un sistema obtener una respuesta a través de messaging cuando los mensajes fluyen en una sola dirección por cada canal? La respuesta de Request-Reply es directa pero con profundas implicaciones: se utilizan dos canales — uno para la petición y otro para la respuesta — coordinados mediante un identificador de correlación que permite al solicitante vincular la respuesta con su petición original.

Este patrón es omnipresente. Cada vez que un microservicio necesita consultar datos de otro servicio a través de un broker, cada vez que un API gateway traduce una llamada HTTP síncrona en messaging asíncrono, cada vez que un sistema de telecomunicaciones consulta el saldo en tiempo real de un cliente a través de una cola de mensajes — está aplicando Request-Reply. Es el puente entre el mundo síncrono que los usuarios y muchos protocolos esperan, y el mundo asíncrono que proporciona resiliencia, desacoplamiento y escalabilidad.


3. Definición Detallada

Propósito

Request-Reply establece un protocolo de comunicación bidireccional sobre messaging unidireccional. Su propósito es permitir que un solicitante (requestor) envíe una petición y reciba una respuesta del destinatario (replier), sin que ambos necesiten una conexión directa punto a punto, y manteniendo todas las ventajas del messaging asíncrono: desacoplamiento, resiliencia ante fallos temporales y capacidad de buffering.

Lógica Arquitectónica

En una invocación síncrona (REST, gRPC), la respuesta viaja de regreso por el mismo canal de comunicación que la petición — la conexión TCP/HTTP se mantiene abierta hasta que llega la respuesta. En messaging, cada canal es unidireccional: el productor deposita un mensaje y el consumidor lo recoge. No hay un "camino de vuelta" implícito.

Request-Reply resuelve esta asimetría introduciendo un segundo canal dedicado a las respuestas. El solicitante:

  1. Genera un identificador único para la petición.
  2. Incluye en el mensaje de petición la dirección del canal de respuesta (Return Address) y el identificador de correlación.
  3. Envía la petición al canal de request.
  4. Escucha en el canal de reply, filtrando por el identificador de correlación.

El destinatario:

  1. Consume la petición del canal de request.
  2. Procesa la petición y genera la respuesta.
  3. Envía la respuesta al canal indicado en la Return Address, incluyendo el identificador de correlación original.

Principio de Diseño Subyacente

El principio es conversación sobre canales unidireccionales. Así como un protocolo de red full-duplex se puede implementar sobre dos canales simplex, Request-Reply implementa interacción bidireccional sobre dos Message Channels unidireccionales. El identificador de correlación es el mecanismo que "une" los dos mensajes en una conversación lógica.

Problema Estructural que Resuelve

Sin Request-Reply, un sistema que necesita obtener una respuesta a través de messaging tiene dos opciones insatisfactorias:

  • Polling: el solicitante envía la petición y luego consulta repetidamente un almacén de datos donde el destinatario deposita la respuesta. Ineficiente, genera carga innecesaria y tiene latencia impredecible.
  • Callback directo: el solicitante expone un endpoint HTTP y el destinatario invoca ese endpoint cuando tiene la respuesta. Esto reintroduce acoplamiento directo y elimina las ventajas del messaging.

Request-Reply proporciona una solución nativa del paradigma de messaging: el solicitante escucha en un canal de respuesta, y el destinatario publica la respuesta en ese canal. No hay polling, no hay conexión directa.

Contexto en el que Emerge

Request-Reply emerge en cualquier escenario donde:

  • Dos sistemas necesitan intercambiar información bidireccionalmente.
  • Se desea mantener messaging como infraestructura de comunicación (por resiliencia, desacoplamiento, auditoría).
  • El solicitante no puede continuar su procesamiento sin la respuesta.
  • Se quiere abstraer múltiples backends detrás de un canal común de peticiones.

Relación con Sistemas Distribuidos

En la teoría de sistemas distribuidos, Request-Reply implementa un protocolo de comunicación tipo RPC (Remote Procedure Call) sobre un canal de mensajes. A diferencia de un RPC tradicional, donde el fallo del servidor resulta en un timeout del cliente, en Request-Reply con messaging la petición persiste en el canal hasta que el servidor esté disponible para procesarla. Esto proporciona una semántica de "RPC con durabilidad" que no existe en protocolos síncronos puros.


4. Problema que Resuelve

El Problema Antes del Patrón

Sin Request-Reply, los sistemas distribuidos que necesitan comunicación bidireccional sobre messaging enfrentan:

  • Ausencia de mecanismo de respuesta: los canales de mensajería son unidireccionales por naturaleza. El productor envía un mensaje y no tiene forma nativa de recibir una respuesta del consumidor.
  • Acoplamiento temporal en alternativas síncronas: si se recurre a HTTP/gRPC para las consultas, se pierde el desacoplamiento temporal que proporciona el messaging. Si el servidor no está disponible, la petición falla inmediatamente.
  • Incapacidad de buffering de peticiones: sin messaging, si el servidor está sobrecargado, las peticiones se rechazan o se pierden. Con Request-Reply sobre messaging, las peticiones se acumulan en el canal y se procesan cuando el servidor tiene capacidad.

Síntomas del Problema

  • Arquitecturas híbridas inconsistentes donde algunos flujos usan messaging y otros recurren a HTTP solo porque necesitan una respuesta, creando dos modelos de comunicación diferentes con diferentes modos de fallo.
  • Sistemas que implementan polling artesanal: el solicitante escribe en una base de datos y el destinatario actualiza esa misma base de datos con la respuesta, con el solicitante consultando periódicamente.
  • Pérdida de peticiones cuando el servidor receptor no está disponible y no hay buffer intermedio.
  • Imposibilidad de aplicar políticas de priorización, throttling o auditoría a las peticiones porque no pasan por el middleware de messaging.

Impacto Operativo y Arquitectónico

Sin Request-Reply implementado correctamente:

  • Las consultas críticas (saldo de cuenta, estado de envío, disponibilidad de inventario) fallan cuando el servicio backend tiene downtime, porque se usan llamadas síncronas directas.
  • No hay visibilidad sobre las peticiones en tránsito: cuántas hay pendientes, cuál es la latencia promedio de respuesta, cuántas expiraron sin respuesta.
  • No se puede aplicar backpressure: si llegan más peticiones de las que el backend puede procesar, no hay buffer.

Riesgos Si No Se Implementa Correctamente

  • Timeout inadecuado: el solicitante espera indefinidamente una respuesta que nunca llegará porque el mensaje se perdió o el consumidor falló.
  • Canal de respuesta sobrecargado: si todos los solicitantes comparten un canal de respuesta sin filtrado eficiente, cada solicitante recibe y descarta mensajes que no le corresponden.
  • Fuga de recursos: el solicitante crea recursos temporales (colas temporales, threads bloqueados) para esperar la respuesta y no los libera correctamente ante timeouts o errores.
  • Correlación incorrecta: sin un Correlation Identifier robusto, las respuestas se vinculan a peticiones equivocadas, produciendo errores de datos difíciles de diagnosticar.

Ejemplos Reales

  • Telecomunicaciones: un sistema de IVR (Interactive Voice Response) consulta el saldo del cliente en tiempo real. La consulta se envía vía messaging al core de billing, que responde con el saldo actual. El IVR necesita la respuesta en menos de 2 segundos para no degradar la experiencia del usuario.
  • Banca: un servicio de autorización de transacciones envía la solicitud de autorización vía messaging al motor de riesgo, que responde con approve/decline. La petición debe persistir aunque el motor de riesgo esté temporalmente sobrecargado.
  • E-commerce: el checkout consulta disponibilidad de inventario en un warehouse management system vía messaging. Si el WMS está procesando un batch de actualización, las consultas se encolan y se responden cuando el WMS tiene capacidad.

5. Contexto de Aplicación

Cuándo Usarlo

  • Cuando se necesita comunicación bidireccional entre sistemas y se desea mantener messaging como infraestructura (por resiliencia, desacoplamiento, auditoría).
  • Cuando el servicio backend puede tener períodos de indisponibilidad y las peticiones deben encolarse en lugar de perderse.
  • Cuando se necesita aplicar políticas de priorización o throttling a las peticiones a nivel de middleware.
  • Cuando múltiples solicitantes necesitan consultar un mismo servicio y se quiere un punto central de ingreso (canal de peticiones) para balanceo y monitoreo.

Cuándo No Usarlo

  • Cuando la latencia de respuesta debe ser sub-milisegundo y el overhead de dos hops de messaging es inaceptable. En estos casos, una llamada directa gRPC/HTTP es más apropiada.
  • Cuando no se necesita durabilidad de las peticiones: si una petición perdida se puede reintentar trivialmente, el overhead de messaging no se justifica.
  • Cuando la comunicación es puramente fire-and-forget (el productor no necesita respuesta).
  • Cuando ya existe una infraestructura de API gateway con circuit breakers y retries que proporciona resiliencia suficiente para llamadas síncronas.

Precondiciones

  • Existe un sistema de mensajería (broker) con soporte para al menos dos canales.
  • Existe un mecanismo para incluir metadata en los mensajes (headers para Return Address y Correlation Identifier).
  • El solicitante puede actuar como consumidor (escuchar en un canal de respuesta), no solo como productor.

Restricciones

  • La latencia mínima es al menos el doble de un single hop de messaging (ida + vuelta).
  • Requiere gestión de timeouts: el solicitante debe definir cuánto tiempo espera la respuesta antes de considerar que falló.
  • El canal de respuesta consume recursos del broker (storage, connections) que deben gestionarse.

Supuestos Arquitectónicos

  • Ambos participantes (requestor y replier) están conectados al mismo sistema de mensajería o a sistemas federados.
  • Los mensajes pueden transportar metadata (headers) además del payload.
  • El solicitante tiene capacidad de bloquear o registrar un callback mientras espera la respuesta.

6. Fuerzas Arquitectónicas

Sincronía vs. Asincronía

La tensión fundamental de Request-Reply. El solicitante necesita un comportamiento "síncrono" (enviar y esperar respuesta), pero la infraestructura es asíncrona. Request-Reply reconcilia ambos mundos, pero el resultado es un patrón inherentemente más complejo que una llamada síncrona directa. La complejidad adicional (dos canales, correlación, timeouts) solo se justifica cuando las ventajas del messaging (durabilidad, desacoplamiento, buffering) son necesarias.

Latencia vs. Resiliencia

Una llamada HTTP directa tiene menor latencia (un round-trip de red) que Request-Reply sobre messaging (producir a canal + consumir del canal + procesar + producir respuesta + consumir respuesta). Sin embargo, Request-Reply es más resiliente: si el servidor está caído, la petición espera en el canal en lugar de fallar inmediatamente. El trade-off es latencia adicional a cambio de resiliencia ante fallos temporales.

Simplicidad vs. Observabilidad

Request-Reply introduce complejidad pero también un punto de observación valioso: los canales de petición y respuesta permiten medir el throughput de peticiones, la latencia de respuesta, la profundidad de la cola de peticiones pendientes y la tasa de timeouts. Esta observabilidad no existe en llamadas directas sin instrumentación adicional.

Acoplamiento vs. Flexibilidad

Con Request-Reply, el solicitante no necesita conocer la dirección del servicio backend — solo el nombre del canal de peticiones. Esto permite reemplazar, escalar o reubicar el backend sin cambiar el solicitante. Sin embargo, se introduce un acoplamiento implícito al formato del mensaje de petición y respuesta, y al nombre del canal.

Escalabilidad vs. Complejidad de Correlación

Si múltiples instancias del solicitante comparten un canal de respuesta, cada instancia debe filtrar las respuestas que le corresponden usando el Correlation Identifier. Si cada instancia crea su propio canal temporal de respuesta, se simplifica la correlación pero se aumenta la carga en el broker (creación/destrucción de canales). Esta tensión se manifiesta constantemente en implementaciones a escala.


7. Estructura Conceptual del Patrón

Actores o Componentes Involucrados

  1. Requestor (Solicitante): la aplicación que envía la petición y espera la respuesta.
  2. Replier (Respondedor): la aplicación que recibe la petición, la procesa y envía la respuesta.
  3. Request Channel: el canal por donde viaja la petición.
  4. Reply Channel: el canal por donde viaja la respuesta.
  5. Correlation Identifier: el identificador que vincula respuesta con petición.
  6. Broker: el sistema de mensajería que gestiona ambos canales.

Flujo Lógico

flowchart TD
    A([Requestor]) --> B[Generar correlation_id\núnico - UUID]
    B --> C[Construir mensaje de petición\nbody + reply_to + correlation_id]
    C --> D[(Request Channel)]
    A --> E[Iniciar timer de timeout]
    A --> F[Escuchar en Reply Channel\nfiltrando por correlation_id]
    D --> G([Replier])
    G --> H[Consumir mensaje del\nRequest Channel]
    H --> I[Procesar petición\nconsulta DB, lógica, etc.]
    I --> J[Construir respuesta\ncon correlation_id copiado]
    J --> K[(Reply Channel)]
    K --> F
    F --> L{correlation_id\ncoincide?}
    L -- Sí --> M[Cancelar timer y\nprocesar respuesta]
    M --> N([Fin])

Responsabilidades

Componente Responsabilidad
Requestor Generar correlation_id, incluir reply_to, enviar petición, escuchar respuesta, gestionar timeout
Replier Consumir petición, procesar, enviar respuesta al reply_to con el correlation_id original
Request Channel Almacenar y entregar peticiones al replier
Reply Channel Almacenar y entregar respuestas al requestor
Broker Gestionar ambos canales, garantizar entrega

Decisiones de Diseño Clave

  1. Reply channel compartido vs. temporal: ¿todos los requestors comparten un reply channel (eficiente en recursos pero requiere filtrado) o cada requestor crea un canal temporal exclusivo (simple en correlación pero costoso en recursos)?
  2. Blocking vs. callback: ¿el requestor bloquea un thread esperando la respuesta o registra un callback/future que se invoca cuando la respuesta llega?
  3. Timeout handling: ¿qué ocurre cuando la respuesta no llega? ¿Se reintenta la petición? ¿Se escala a un sistema alternativo? ¿Se reporta error?
  4. Garantía de entrega: ¿la petición debe persistirse en el canal (durable) o puede ser in-memory (transient)?

8. Ejemplo Arquitectónico Detallado

Dominio: Telecomunicaciones — Consulta de Saldo en Tiempo Real

Contexto del Negocio

Un operador de telecomunicaciones con 25 millones de clientes prepago opera un sistema de consulta de saldo que atiende múltiples canales de contacto: app móvil, USSD (#123#), IVR (llamada al 611), portal web y agentes de call center. Cada canal necesita consultar el saldo actual del cliente en tiempo real, con una latencia máxima aceptable de 2 segundos.

El core de billing que gestiona los saldos es un sistema legacy basado en Oracle que soporta un máximo de 500 consultas concurrentes. En horas pico (lunes a las 9:00 AM, primer día del mes), los canales de contacto generan hasta 3,000 consultas por segundo. Sin buffering, el core de billing colapsa.

Necesidad de Integración

Los canales de contacto necesitan consultar el saldo en tiempo real pero no pueden invocar directamente al core de billing porque:

  1. El core no soporta la carga directa de todos los canales simultáneamente.
  2. Si el core está en mantenimiento (ventana de 30 minutos cada noche), las peticiones deben encolarse, no fallar.
  3. Se necesita priorización: las consultas del IVR (cliente en llamada, experiencia real-time) tienen mayor prioridad que las del portal web.
  4. Se necesita auditoría centralizada de todas las consultas de saldo para compliance regulatorio.

Sistemas Involucrados

  1. Mobile App Backend: API REST que atiende la app móvil.
  2. USSD Gateway: gateway que procesa códigos USSD.
  3. IVR System: sistema de respuesta de voz interactiva.
  4. Web Portal Backend: API REST del portal web.
  5. Call Center CRM: aplicación de agentes de call center.
  6. Balance Query Service: microservicio que media entre los canales y el core de billing.
  7. Core Billing (Oracle): sistema legacy que gestiona saldos.
  8. RabbitMQ Cluster: broker de mensajería.

Diseño de Canales

Canal (Queue RabbitMQ) Dirección Productor Consumidor Prioridad
billing.balance.request.high Request IVR, USSD Balance Query Service Alta
billing.balance.request.normal Request App, Web, CRM Balance Query Service Normal
billing.balance.reply Reply Balance Query Service Todos los solicitantes N/A

Decisiones Arquitectónicas

  1. Dos colas de request por prioridad: las consultas del IVR y USSD (usuario esperando en tiempo real) se enrutan a una cola de alta prioridad que el Balance Query Service consume primero.
  2. Cola de reply compartida con filtrado por correlation_id: todos los solicitantes escuchan en la misma cola de reply pero cada uno filtra por su correlation_id. RabbitMQ no soporta filtrado nativo por header en consumers, por lo que el Balance Query Service publica la respuesta en una cola temporal exclusiva del requestor (reply-to queue).
  3. Colas temporales exclusivas como reply channel: cada instancia del requestor crea una cola temporal (amq.gen-*) al conectarse. Esta cola se usa como reply_to. Cuando la instancia se desconecta, la cola se elimina automáticamente.
  4. Timeout de 2 segundos: si la respuesta no llega en 2 segundos, el requestor devuelve un mensaje genérico al canal de contacto ("Consulta temporalmente no disponible") y registra el timeout para monitoreo.

Riesgos y Mitigaciones

Riesgo Mitigación
Core billing lento → timeouts masivos Circuit breaker en Balance Query Service; cache de saldos recientes (TTL 30s)
Cola de request acumula miles de peticiones Monitoring de queue depth + alertas; auto-scaling del Balance Query Service
Cola temporal del requestor se elimina antes de recibir la respuesta TTL de respuesta en el mensaje (message expiration)
Respuesta llega después del timeout El requestor descarta respuestas tardías; log para análisis de latencia
Pérdida de mensajes de petición Colas durables con persistent delivery mode

9. Desarrollo Paso a Paso del Ejemplo

Paso 1: Petición desde el IVR

Un cliente llama al *611. El IVR le solicita su número de teléfono (MSISDN: +57-300-123-4567) y ofrece la opción "Consultar saldo". El IVR System genera la petición:

{
  "msisdn": "+573001234567",
  "query_type": "current_balance",
  "channel": "ivr",
  "timestamp": "2026-04-07T09:15:32.847Z"
}

Headers del mensaje:

message_id: "msg-a1b2c3d4-e5f6-7890-abcd-ef1234567890"
correlation_id: "corr-7f3e2a91-4b8c-4d2e-9a1f-8c5d3b6e7f90"
reply_to: "amq.gen-IVR-instance-42-reply"
priority: 8
content_type: "application/json"
expiration: "2000"  (TTL: 2 segundos)

El IVR System publica este mensaje en la cola billing.balance.request.high (prioridad alta porque el cliente está en llamada).

Paso 2: Encolamiento y Priorización

RabbitMQ almacena el mensaje en billing.balance.request.high. El Balance Query Service consume de ambas colas de request, pero da preferencia a billing.balance.request.high usando un basic.consume con prefetch=1 y consumiendo primero de la cola de alta prioridad.

En este momento hay 47 peticiones en la cola normal y 3 en la cola de alta prioridad. El servicio procesa las 3 de alta prioridad primero.

Paso 3: Procesamiento por el Balance Query Service

El Balance Query Service consume la petición del IVR:

  1. Lee el msisdn del body: +573001234567.
  2. Consulta un cache Redis para verificar si hay un saldo reciente (TTL < 30s). No hay cache hit.
  3. Invoca el core de billing Oracle vía JDBC con la consulta de saldo.
  4. Oracle retorna: saldo = 15,420 COP, fecha último recargo = 2026-04-05.
  5. Almacena el resultado en Redis con TTL 30 segundos (para peticiones duplicadas en ventana corta).
  6. Construye el mensaje de respuesta.

Paso 4: Envío de la Respuesta

El Balance Query Service publica la respuesta en la cola indicada en reply_to (amq.gen-IVR-instance-42-reply):

{
  "msisdn": "+573001234567",
  "balance_cop": 15420,
  "last_recharge": "2026-04-05T18:30:00Z",
  "balance_expiry": "2026-05-05T23:59:59Z",
  "query_status": "success"
}

Headers de la respuesta:

correlation_id: "corr-7f3e2a91-4b8c-4d2e-9a1f-8c5d3b6e7f90"
content_type: "application/json"
timestamp: "2026-04-07T09:15:33.124Z"

El correlation_id de la respuesta es idéntico al de la petición. Esto permite al IVR vincular la respuesta con la petición original.

Paso 5: Recepción por el IVR

El IVR System está escuchando en su cola temporal amq.gen-IVR-instance-42-reply. Recibe la respuesta, verifica que el correlation_id coincide con una petición pendiente, cancela el timer de timeout (solo habían transcurrido 277ms de los 2000ms permitidos) y reproduce al cliente: "Su saldo actual es quince mil cuatrocientos veinte pesos, válido hasta el cinco de mayo".

Paso 6: Escenario de Timeout

Simultáneamente, el portal web envió una consulta de saldo para otro cliente. La petición se encoló en billing.balance.request.normal. El core de billing estaba procesando un batch de reconciliación y la consulta tardó 3.2 segundos. El Web Portal Backend tenía un timeout de 2 segundos. Al expirar el timeout:

  1. El backend web devuelve al usuario: "No pudimos obtener tu saldo en este momento. Intenta de nuevo en unos segundos."
  2. Registra el timeout con el correlation_id para monitoreo.
  3. Deja de escuchar en la cola temporal de reply.
  4. Cuando la respuesta finalmente llega (1.2 segundos después del timeout), la cola temporal ya fue eliminada y RabbitMQ descarta el mensaje (o lo envía a dead-letter si está configurado).

10. Diagrama Técnico del Patrón

Código Python con diagrams

Diagrama General

Diagrama AWS

Diagrama Azure

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 Oracle
from diagrams.onprem.inmemory import Redis
from diagrams.onprem.monitoring import Grafana
from diagrams.onprem.network import Nginx

with Diagram("Request-Reply - Telecom Balance Query", show=False, direction="LR"):

    with Cluster("Contact Channels (Requestors)"):
        ivr = Server("IVR System")
        ussd = Server("USSD Gateway")
        app = Server("Mobile App\nBackend")
        web = Server("Web Portal\nBackend")
        crm = Server("Call Center\nCRM")

    with Cluster("RabbitMQ Cluster"):
        req_high = RabbitMQ("billing.balance\n.request.high")
        req_normal = RabbitMQ("billing.balance\n.request.normal")
        reply_ivr = RabbitMQ("amq.gen-IVR\n(temp reply)")
        reply_app = RabbitMQ("amq.gen-App\n(temp reply)")

    with Cluster("Processing"):
        balance_svc = Server("Balance Query\nService")
        cache = Redis("Balance\nCache (30s TTL)")

    with Cluster("Backend"):
        billing = Oracle("Core Billing\n(Oracle)")

    monitoring = Grafana("Request-Reply\nMonitoring")

    # Request flow (high priority)
    ivr >> Edge(label="request", color="red") >> req_high
    ussd >> Edge(label="request", color="red") >> req_high

    # Request flow (normal priority)
    app >> Edge(label="request", color="blue") >> req_normal
    web >> Edge(label="request", color="blue") >> req_normal
    crm >> Edge(label="request", color="blue") >> req_normal

    # Processing
    req_high >> Edge(label="consume\n(priority)") >> balance_svc
    req_normal >> Edge(label="consume") >> balance_svc
    balance_svc >> cache
    balance_svc >> billing

    # Reply flow
    balance_svc >> Edge(label="reply", style="dashed", color="green") >> reply_ivr
    balance_svc >> Edge(label="reply", style="dashed", color="green") >> reply_app
    reply_ivr >> Edge(style="dashed", color="green") >> ivr
    reply_app >> Edge(style="dashed", color="green") >> app

    # Monitoring
    req_high >> Edge(style="dotted") >> monitoring
    req_normal >> Edge(style="dotted") >> monitoring
from diagrams import Diagram, Cluster, Edge
from diagrams.aws.compute import Lambda, ECS
from diagrams.aws.database import ElasticacheForRedis, RDS
from diagrams.aws.integration import SQS
from diagrams.aws.management import Cloudwatch


with Diagram("Request-Reply - Telecom Balance Query (AWS)", show=False, direction="LR"):

    with Cluster("Contact Channels (Requestors)"):
        ivr = ECS("IVR System")
        ussd = ECS("USSD Gateway")
        app = Lambda("Mobile App\nBackend")
        web = Lambda("Web Portal\nBackend")
        crm = ECS("Call Center\nCRM")

    with Cluster("SQS Request Queues"):
        req_high = SQS("billing.balance\n.request.high")
        req_normal = SQS("billing.balance\n.request.normal")
        reply_ivr = SQS("reply-ivr\n(temp reply)")
        reply_app = SQS("reply-app\n(temp reply)")

    with Cluster("Processing"):
        balance_svc = Lambda("Balance Query\nService")
        cache = ElasticacheForRedis("ElastiCache\n(30s TTL)")

    with Cluster("Backend"):
        billing = RDS("Core Billing\n(RDS Oracle)")

    monitoring = Cloudwatch("Request-Reply\nMonitoring")

    # Request flow (high priority)
    ivr >> Edge(label="request", color="red") >> req_high
    ussd >> Edge(label="request", color="red") >> req_high

    # Request flow (normal priority)
    app >> Edge(label="request", color="blue") >> req_normal
    web >> Edge(label="request", color="blue") >> req_normal
    crm >> Edge(label="request", color="blue") >> req_normal

    # Processing
    req_high >> Edge(label="consume\n(priority)") >> balance_svc
    req_normal >> Edge(label="consume") >> balance_svc
    balance_svc >> cache
    balance_svc >> billing

    # Reply flow
    balance_svc >> Edge(label="reply", style="dashed", color="green") >> reply_ivr
    balance_svc >> Edge(label="reply", style="dashed", color="green") >> reply_app
    reply_ivr >> Edge(style="dashed", color="green") >> ivr
    reply_app >> Edge(style="dashed", color="green") >> app

    # Monitoring
    req_high >> Edge(style="dotted") >> monitoring
    req_normal >> Edge(style="dotted") >> monitoring
from diagrams import Diagram, Cluster, Edge
from diagrams.azure.compute import FunctionApps
from diagrams.azure.database import CacheForRedis, SQLServers
from diagrams.azure.devops import ApplicationInsights
from diagrams.azure.integration import ServiceBus


with Diagram("Request-Reply - Telecom Balance Query (Azure)", show=False, direction="LR"):

    with Cluster("Contact Channels (Requestors)"):
        ivr = FunctionApps("IVR System")
        ussd = FunctionApps("USSD Gateway")
        app = FunctionApps("Mobile App\nBackend")
        web = FunctionApps("Web Portal\nBackend")
        crm = FunctionApps("Call Center\nCRM")

    with Cluster("Service Bus (Request-Reply)"):
        req_high = ServiceBus("billing.balance\n.request.high\n(Queue)")
        req_normal = ServiceBus("billing.balance\n.request.normal\n(Queue)")
        reply_ivr = ServiceBus("reply.ivr\n(Session Queue)")
        reply_app = ServiceBus("reply.app\n(Session Queue)")

    with Cluster("Processing"):
        balance_svc = FunctionApps("Balance Query\nService")
        cache = CacheForRedis("Azure Redis\nCache (30s TTL)")

    with Cluster("Backend"):
        billing = SQLServers("Azure SQL\n(Core Billing)")

    monitoring = ApplicationInsights("Application\nInsights")

    # Request flow (high priority)
    ivr >> Edge(label="request\nSessionId", color="red") >> req_high
    ussd >> Edge(label="request\nSessionId", color="red") >> req_high

    # Request flow (normal priority)
    app >> Edge(label="request\nSessionId", color="blue") >> req_normal
    web >> Edge(label="request\nSessionId", color="blue") >> req_normal
    crm >> Edge(label="request\nSessionId", color="blue") >> req_normal

    # Processing
    req_high >> Edge(label="consume\n(priority)") >> balance_svc
    req_normal >> Edge(label="consume") >> balance_svc
    balance_svc >> cache
    balance_svc >> billing

    # Reply flow
    balance_svc >> Edge(label="reply\nSessionId", style="dashed", color="green") >> reply_ivr
    balance_svc >> Edge(label="reply\nSessionId", style="dashed", color="green") >> reply_app
    reply_ivr >> Edge(style="dashed", color="green") >> ivr
    reply_app >> Edge(style="dashed", color="green") >> app

    # Monitoring
    req_high >> Edge(style="dotted") >> monitoring
    req_normal >> Edge(style="dotted") >> monitoring

Explicación del Diagrama

El diagrama ilustra la arquitectura de Request-Reply para consultas de saldo en telecomunicaciones:

  1. Contact Channels actúan como requestors: envían peticiones de saldo a las colas de request.
  2. Dos colas de request separadas por prioridad: billing.balance.request.high (IVR, USSD) y billing.balance.request.normal (App, Web, CRM).
  3. El Balance Query Service consume de ambas colas, priorizando la cola high.
  4. El servicio consulta un cache Redis y, si no hay hit, invoca al Core Billing Oracle.
  5. Las respuestas se envían a las colas temporales de reply de cada requestor (indicadas en el header reply_to).
  6. Cada requestor recibe su respuesta de su cola temporal exclusiva.
  7. Grafana monitorea queue depth, latencia y timeouts.

Correspondencia Patrón ↔ Diagrama

Concepto del Patrón Componente del Diagrama
Requestor IVR, USSD, App, Web, CRM
Replier Balance Query Service
Request Channel billing.balance.request.high/normal
Reply Channel amq.gen-* (colas temporales por requestor)
Correlation Identifier Header correlation_id en request y reply
Return Address Header reply_to apuntando a la cola temporal

11. Beneficios

Impacto Técnico

  • Desacoplamiento: los canales de contacto no conocen la dirección del core de billing ni del Balance Query Service. Solo conocen el nombre de la cola de request y su propia cola de reply.
  • Buffering de peticiones: durante picos de carga, las peticiones se encolan en lugar de rechazarse. El core de billing procesa a su capacidad máxima (500 concurrentes) sin recibir más carga de la que puede manejar.
  • Priorización: las colas separadas permiten que las peticiones del IVR (cliente en llamada) se procesen antes que las del portal web (cliente puede esperar o reintentar).
  • Resiliencia: si el Balance Query Service se reinicia, las peticiones pendientes siguen en la cola y se procesan cuando el servicio vuelve.

Impacto Organizacional

  • El equipo de IVR, el equipo de la app móvil y el equipo del portal web pueden evolucionar sus sistemas independientemente. El contrato compartido es el formato del mensaje de petición y respuesta.
  • El equipo de billing puede reemplazar el core Oracle por un nuevo sistema sin afectar a los canales de contacto — solo el Balance Query Service se actualiza.

Impacto Operacional

  • Observabilidad de extremo a extremo: la profundidad de las colas de request indica la carga actual sobre billing; la latencia entre envío de request y recepción de reply mide la experiencia del usuario; la tasa de timeouts indica degradación del backend.
  • Capacity planning: el histórico de queue depth permite predecir cuándo se necesita escalar el Balance Query Service.

12. Desventajas y Riesgos

Complejidad Añadida

  • Dos canales en lugar de uno: cada interacción Request-Reply requiere gestionar un canal de petición y un canal de respuesta (o N canales de respuesta si se usan colas temporales).
  • Gestión de timeouts: el solicitante debe implementar lógica de timeout, cancelación y manejo de respuestas tardías. Esto es significativamente más complejo que un HTTP call con timeout nativo.
  • Correlación: se debe implementar correctamente el mecanismo de Correlation Identifier para vincular respuestas con peticiones.

Riesgos de Mal Uso

  • Request-Reply donde no se necesita: usar Request-Reply para operaciones que pueden ser fire-and-forget añade complejidad sin beneficio.
  • Síncrono disfrazado: implementar Request-Reply con un thread bloqueado esperando la respuesta es, funcionalmente, una llamada síncrona con mayor latencia y complejidad. Si no se aprovecha la asincronía (non-blocking wait, futures), se obtiene lo peor de ambos mundos.
  • Ignorar respuestas tardías: no implementar limpieza de respuestas que llegan después del timeout produce memory leaks o colas que crecen indefinidamente.

Costos de Operación

  • Recursos del broker: colas temporales de reply consumen recursos del broker (memoria, file descriptors, connections).
  • Complejidad de monitoreo: se deben monitorear las colas de request y las colas de reply por separado, más las métricas de correlación (latencia, timeout rate).

Anti-Patterns

  • Request-Reply sin timeout: esperar indefinidamente una respuesta que puede no llegar nunca. Todo Request-Reply debe tener un timeout explícito.
  • Shared reply queue sin correlación: múltiples requestors compartiendo una cola de reply sin Correlation Identifier, resultando en respuestas entregadas al requestor equivocado.

13. Relación con Otros Patrones

Patrones Complementarios

  • Return Address (este capítulo): define el mecanismo específico por el cual el requestor indica dónde enviar la respuesta. Return Address es un componente esencial de Request-Reply.
  • Correlation Identifier (este capítulo): define el mecanismo para vincular la respuesta con la petición original. Sin Correlation Identifier, Request-Reply no puede funcionar cuando hay múltiples peticiones concurrentes.
  • Message Expiration (este capítulo): define el TTL del mensaje de petición y/o respuesta, implementando el timeout del Request-Reply a nivel de infraestructura.

Patrones que Suelen Aparecer Antes o Después

  • Antes: Command Message o Document Message — la petición es típicamente un Command ("consulta el saldo") o un Document ("aquí están los datos para la consulta").
  • Después: Content-Based Router o Message Translator — la respuesta puede necesitar routing o transformación antes de llegar al solicitante final.

Combinaciones Comunes

  • Request-Reply + Competing Consumers: múltiples instancias del replier consumen peticiones del canal de request en paralelo. Cada instancia procesa una petición y envía la respuesta.
  • Request-Reply + Dead Letter Channel: peticiones que no se pueden procesar se envían a dead-letter para análisis posterior, en lugar de descartarse silenciosamente.
  • Request-Reply + Wire Tap: las peticiones y respuestas se interceptan para auditoría o logging sin afectar el flujo principal.

Diferencias con Patrones Similares

  • vs. RPC síncrono: Request-Reply añade durabilidad (la petición persiste en el canal) y desacoplamiento (el requestor no conoce la dirección del replier), a cambio de mayor latencia y complejidad.
  • vs. Event Message + eventual query: en lugar de request-reply, algunos diseños emiten un evento y el interesado consulta un read model. Esto elimina la bidireccionalidad pero introduce eventual consistency.

14. Relevancia Actual del Patrón

Evaluación: Relevancia Alta

Argumentación

Request-Reply sigue siendo uno de los patrones más utilizados en arquitecturas modernas porque la necesidad de comunicación bidireccional es inherente a la mayoría de los sistemas distribuidos. Aunque las arquitecturas event-driven y reactive reducen la necesidad de request-reply en favor de eventos y proyecciones, hay escenarios donde una respuesta directa es imprescindible:

  • Consultas en tiempo real: saldos, estados, disponibilidad — el usuario espera una respuesta inmediata.
  • Validaciones: verificación de identidad, autorización de transacciones — el flujo no puede continuar sin la respuesta.
  • Orquestación de sagas: el orquestador envía comandos a los participantes y espera confirmaciones.

Cómo Se Implementa Hoy

Plataforma Mecanismo de Request-Reply
RabbitMQ reply_to header + temporary exclusive queue (amq.gen-*)
Kafka Reply topic con consumer filtering por correlation_id; KafkaReplyingTemplate en Spring
Azure Service Bus ReplyTo + ReplyToSessionId para correlación basada en sesiones
AWS SQS Reply queue separada + correlation_id en message attributes
gRPC sobre messaging Request como Command Message, reply como Event/Document en reply channel
NATS Request-Reply nativo con nats.Request() y inbox subjects

Qué Parte Sigue Siendo Esencial

  • El concepto de dos canales coordinados (request + reply) es universal.
  • El Correlation Identifier como mecanismo de vinculación es esencial en toda implementación.
  • La gestión de timeouts sigue siendo el aspecto más crítico y más frecuentemente mal implementado.

15. Implementación en Arquitecturas Modernas

RabbitMQ (AMQP 0-9-1)

Request:
  Exchange: "" (default)
  Routing Key: "billing.balance.request"
  Properties:
    reply_to: "amq.gen-Xa1b2c3d4"  (auto-generated exclusive queue)
    correlation_id: "corr-7f3e2a91"
    expiration: "2000"

Reply:
  Exchange: "" (default)
  Routing Key: "amq.gen-Xa1b2c3d4"  (el valor de reply_to)
  Properties:
    correlation_id: "corr-7f3e2a91"  (copiado de la petición)

RabbitMQ tiene soporte nativo para Request-Reply con colas temporales exclusivas. El requestor crea una cola anónima (amq.gen-*), la incluye en reply_to, y el replier envía la respuesta a esa cola. La cola se destruye automáticamente cuando el requestor se desconecta.

Apache Kafka

Request Topic: billing.balance.request
  Headers:
    kafka_replyTopic: "billing.balance.reply"
    kafka_replyPartition: 2
    kafka_correlationId: "corr-7f3e2a91"

Reply Topic: billing.balance.reply
  Partition: 2
  Headers:
    kafka_correlationId: "corr-7f3e2a91"

En Kafka, Request-Reply es menos natural porque Kafka no soporta colas temporales. Spring Kafka proporciona ReplyingKafkaTemplate que automatiza el patrón: crea un consumer en el reply topic, filtra por correlation_id, y resuelve un Future cuando llega la respuesta.

Azure Service Bus

Request Queue: billing-balance-request
  Properties:
    ReplyTo: "billing-balance-reply"
    CorrelationId: "corr-7f3e2a91"
    ReplyToSessionId: "session-requestor-42"
    TimeToLive: 00:00:02

Reply Queue: billing-balance-reply (session-enabled)
  Session: "session-requestor-42"
  Properties:
    CorrelationId: "corr-7f3e2a91"

Azure Service Bus soporta Request-Reply con sessions: el requestor usa ReplyToSessionId para que sus respuestas se agrupen en una session específica, permitiendo correlación eficiente sin escanear todos los mensajes de la cola de reply.

NATS

reply, err := nc.Request("billing.balance.request", payload, 2*time.Second)

NATS tiene Request-Reply como primitiva de primera clase. Request() crea automáticamente un inbox subject, lo incluye como reply_to, envía el mensaje y espera la respuesta con timeout integrado.


16. Consideraciones de Gobierno y Operación

Observabilidad

  • Métricas clave: request rate (peticiones/segundo), reply rate, timeout rate, latencia de round-trip (tiempo entre envío de request y recepción de reply), queue depth de colas de request.
  • Distributed tracing: propagar el trace_id de OpenTelemetry en los headers del mensaje para trazar la petición completa a través del request channel, el processing y el reply channel.
  • Alertas: timeout rate > 5% indica degradación del backend; queue depth > umbral indica saturación; reply rate < request rate indica pérdida de respuestas.

Monitoreo

  • Consumer lag en colas de request: indica cuántas peticiones están pendientes de procesamiento.
  • Edad del mensaje más antiguo en la cola de request: indica el tiempo de espera máximo actual.
  • Colas temporales de reply: monitorear el número de colas temporales activas para detectar leaks (colas que no se destruyen).

Timeout Policy

  • Definir timeouts por tipo de operación: consultas simples (2s), operaciones complejas (10s), reportes (60s).
  • Implementar timeout tanto en el requestor (timer local) como en el mensaje (TTL/expiration) para que mensajes expirados no se procesen innecesariamente.
  • Registrar todos los timeouts con el correlation_id para análisis de causas.

Seguridad

  • Autenticación y autorización en ambos canales (request y reply).
  • Validar que el requestor tiene permiso para enviar peticiones al request channel.
  • Considerar cifrado end-to-end del payload si los datos son sensibles (ej. saldos financieros).

Idempotencia

  • El replier debe ser idempotente: si la misma petición llega dos veces (por retry del requestor), debe retornar la misma respuesta sin efectos secundarios duplicados.
  • Usar el message_id o correlation_id como clave de deduplicación.

17. Errores Comunes

No Implementar Timeout

El error más peligroso. Un requestor sin timeout puede quedar bloqueado indefinidamente esperando una respuesta que nunca llegará (porque el replier falló, el mensaje se perdió, o la cola de reply fue eliminada). Todo Request-Reply debe tener un timeout explícito, tanto a nivel de código (timer) como a nivel de mensaje (TTL).

Usar Request-Reply Para Todo

No toda interacción requiere una respuesta. Operaciones como "registrar evento", "enviar notificación" o "actualizar cache" son fire-and-forget por naturaleza. Forzar Request-Reply donde no se necesita duplica la complejidad sin beneficio.

Shared Reply Queue Sin Correlación Eficiente

Si múltiples requestors comparten una cola de reply y cada uno debe escanear todos los mensajes para encontrar los suyos, el overhead crece linealmente con el número de requestors. Solución: usar colas temporales exclusivas (RabbitMQ), sessions (Azure Service Bus) o particiones asignadas (Kafka).

No Limpiar Recursos de Reply

Colas temporales, consumers, timers y futures asociados a un Request-Reply deben limpiarse tanto en el caso de éxito como en el caso de timeout o error. La falta de limpieza produce leaks que degradan el broker y la aplicación gradualmente.

Confundir Latencia de Messaging con Latencia de API

Request-Reply sobre messaging siempre será más lento que una llamada HTTP directa. Si la latencia sub-milisegundo es crítica y no se necesitan las ventajas del messaging (buffering, durabilidad, desacoplamiento), una llamada directa es más apropiada.

No Manejar Respuestas Tardías

Después de un timeout, la respuesta puede llegar eventualmente. Si el requestor ya liberó los recursos (destruyó la cola temporal, canceló el future), no hay problema. Pero si la cola de reply persiste y acumula respuestas tardías sin consumirlas, se genera un leak de mensajes.


18. Conclusión Técnica

Request-Reply es el patrón que reconcilia la necesidad de comunicación bidireccional con las ventajas del messaging asíncrono. Su implementación requiere coordinar dos canales, un mecanismo de correlación y una política de timeout — más complejidad que una llamada síncrona directa, pero a cambio proporciona durabilidad de peticiones, desacoplamiento de sistemas, buffering ante picos de carga y un punto de observación centralizado.

Cuándo aporta valor: cuando la interacción requiere una respuesta y se necesitan las ventajas del messaging — durabilidad (peticiones que sobreviven caídas del servidor), desacoplamiento (el requestor no conoce la dirección del replier), buffering (peticiones encoladas durante picos) o auditoría (todas las peticiones y respuestas pasan por el broker).

Cuándo evita problemas importantes: en sistemas donde el backend tiene capacidad limitada (como el core de billing del ejemplo), Request-Reply con messaging previene la saturación directa del backend al actuar como buffer y control de flujo. Las peticiones se encolan y se procesan al ritmo que el backend puede manejar.

Cuándo no conviene adoptarlo: cuando la latencia mínima es crítica y no se necesitan las garantías del messaging. Una consulta que debe responder en microsegundos (cache lookup, in-memory computation) no se beneficia de Request-Reply sobre messaging. Tampoco conviene cuando la interacción es puramente unidireccional.

Recomendación para arquitectos: implemente Request-Reply solo cuando las ventajas del messaging superen el costo de la complejidad adicional. Cuando lo implemente, trate los tres componentes — Return Address, Correlation Identifier y Timeout — como requisitos no negociables, no como opciones. Monitoree la tasa de timeouts como indicador primario de salud del sistema y diseñe el reply channel considerando la escala de requestors concurrentes (colas temporales para escala moderada, particiones o sessions para escala alta).