Saltar a contenido

Scatter-Gather

1. Nombre del Patrón

  • Nombre oficial: Scatter-Gather
  • Categoría: Message Routing (Enrutamiento de Mensajes)
  • Traducción contextual: Dispersión y Recolección

2. Resumen Ejecutivo

Scatter-Gather es un patrón de enrutamiento compuesto que resuelve el problema de consultar múltiples fuentes en paralelo y combinar sus respuestas en un único resultado consolidado. El patrón opera en dos fases: primero dispersa (scatter) una solicitud a múltiples destinatarios simultáneamente, y luego recolecta (gather) las respuestas de todos ellos, aplicando una lógica de agregación para producir un mensaje de resultado unificado.

El problema fundamental que aborda es la necesidad de obtener información de múltiples proveedores, servicios o sistemas de forma eficiente y consolidada. Sin este patrón, la alternativa sería consultar secuencialmente cada fuente, lo que incrementa dramáticamente la latencia total (suma de latencias individuales en lugar del máximo de latencias individuales), y luego implementar manualmente la lógica de combinación de resultados.

Scatter-Gather combina internamente dos patrones: un Recipient List o Publish-Subscribe Channel para la fase de dispersión, y un Aggregator para la fase de recolección. Su valor reside en encapsular esta composición como una unidad lógica con semántica propia: la consulta paralela con agregación de resultados.

En arquitecturas modernas, Scatter-Gather aparece en GraphQL federation (consulta a múltiples subgraphs), parallel API calls en API gateways, fan-out/fan-in en AWS Step Functions, y consultas distribuidas en sistemas como Elasticsearch (cada shard procesa la query en paralelo y un nodo coordinador agrega los resultados).


3. Definición Detallada

Propósito

El propósito de Scatter-Gather es permitir la consulta paralela a múltiples fuentes y la agregación de sus respuestas en un resultado consolidado, minimizando la latencia total y encapsulando la complejidad de coordinación.

Lógica Arquitectónica

La lógica es análoga a un proceso de licitación: una empresa que necesita un servicio envía la solicitud a múltiples proveedores simultáneamente, espera un tiempo definido para recibir propuestas, y luego evalúa todas las propuestas recibidas para seleccionar la mejor o combinarlas. El beneficio es claro: enviar la solicitud a todos los proveedores al mismo tiempo reduce el tiempo total de respuesta y permite comparar opciones.

En términos de mensajería:

  • Scatter (Dispersión): un componente recibe un mensaje de solicitud y lo difunde a N destinatarios en paralelo. Esta difusión puede ser estática (siempre los mismos destinatarios) o dinámica (los destinatarios se determinan en runtime).
  • Gather (Recolección): un Aggregator recolecta las N respuestas (o un subconjunto si hay timeouts), aplica una lógica de selección o combinación, y produce un único mensaje de resultado.

Principio de Diseño Subyacente

El principio es paralelismo con agregación controlada. En lugar de serializar consultas que son inherentemente independientes, se ejecutan en paralelo para minimizar la latencia, y se centraliza la lógica de combinación en un único punto (el Aggregator) para mantener la coherencia del resultado.

Variantes del Patrón

Existen dos variantes principales:

  1. Scatter-Gather con Recipient List: la solicitud se envía a una lista específica de destinatarios (potencialmente dinámica). Cada destinatario recibe exactamente la misma solicitud. Útil cuando se conocen los destinatarios de antemano.

  2. Scatter-Gather con Auction (subasta): la solicitud se publica en un canal de tipo Publish-Subscribe. Cualquier suscriptor interesado puede responder. El número de respuestas no se conoce de antemano. Útil en escenarios de descubrimiento donde los proveedores se suscriben y desuscriben dinámicamente.

Problema Estructural que Resuelve

Sin Scatter-Gather, la consulta a múltiples fuentes se implementa típicamente como una secuencia:

resultado_1 = consultar(proveedor_1, solicitud)  // 200ms
resultado_2 = consultar(proveedor_2, solicitud)  // 350ms
resultado_3 = consultar(proveedor_3, solicitud)  // 150ms
// Latencia total: 700ms (suma)
mejor = seleccionar_mejor(resultado_1, resultado_2, resultado_3)

Con Scatter-Gather:

scatter(solicitud, [proveedor_1, proveedor_2, proveedor_3])  // paralelo
resultados = gather(timeout=500ms)  // espera máximo 500ms
// Latencia total: ~350ms (máximo individual)
mejor = seleccionar_mejor(resultados)

La diferencia entre la suma y el máximo de latencias puede ser dramática cuando hay muchos destinatarios.

Por Qué No Es Trivial

Las decisiones de diseño del Scatter-Gather son complejas:

  • Timeout y resultados parciales: ¿qué ocurre si solo 2 de 5 proveedores responden dentro del timeout? ¿Se acepta un resultado parcial o se falla completamente?
  • Correlation: ¿cómo se correlacionan las respuestas con la solicitud original? Se necesita un correlation ID que vincule cada respuesta a la solicitud scatter.
  • Lógica de agregación: ¿se selecciona el mejor resultado (best-of-N)? ¿Se combinan todos (merge)? ¿Se descarta alguno según criterios de calidad?
  • Respuestas duplicadas: ¿qué ocurre si un destinatario responde más de una vez (por retry)?
  • Orden de respuestas: las respuestas llegan en orden impredecible. El Aggregator debe ser independiente del orden.

4. Problema que Resuelve

El Problema Antes del Patrón

Sin Scatter-Gather, la consulta a múltiples fuentes se resuelve con patrones inadecuados:

Consulta secuencial: cada fuente se consulta una tras otra. La latencia total es la suma de todas las latencias individuales. Para 5 fuentes con latencia promedio de 200ms, la latencia total es ~1 segundo. Inaceptable para muchas aplicaciones.

Consulta manual en paralelo: el desarrollador implementa manualmente threads o coroutines para consultar en paralelo, gestiona timeouts individuales, implementa la lógica de recolección y combinación. Esta lógica se repite en cada lugar donde se necesita consulta paralela, sin estandarización de timeout handling ni error handling.

Consulta con caché estático: se evita la consulta en tiempo real manteniendo una caché de respuestas precalculadas. Esto elimina la latencia pero introduce el problema de stale data y no funciona para solicitudes dinámicas (como cotizaciones de seguros con parámetros variables).

Síntomas del Problema

  • Latencia excesiva en operaciones que consultan múltiples fuentes secuencialmente.
  • Código de coordinación paralela duplicado en múltiples flujos de negocio.
  • Timeout handling inconsistente entre diferentes puntos de consulta paralela.
  • Resultados incompletos o errores no manejados cuando una de varias fuentes falla.
  • Imposibilidad de añadir o eliminar fuentes sin modificar el código de orquestación.

Impacto Operativo y Arquitectónico

  • Latencia degradada: la latencia crece linealmente con el número de fuentes si la consulta es secuencial.
  • Fragilidad: la falla de una fuente puede bloquear toda la operación si no hay timeout handling adecuado.
  • Acoplamiento: el componente que orquesta la consulta conoce todas las fuentes y su protocolo de comunicación.
  • Imposibilidad de escalar fuentes: añadir un nuevo proveedor requiere modificar el código de orquestación.

Ejemplos Reales

  • Seguros: solicitud de cotización enviada a múltiples underwriters. Se necesita la mejor oferta en tiempo real.
  • E-commerce: búsqueda de producto que consulta múltiples proveedores de inventario para encontrar el mejor precio y disponibilidad.
  • Viajes: búsqueda de vuelos que consulta múltiples aerolíneas y consolidadores en paralelo.
  • Finanzas: solicitud de cotización (RFQ) enviada a múltiples market makers para obtener el mejor precio de ejecución.
  • Search engines: Elasticsearch scatter la query a todos los shards del índice y gather los resultados parciales en un resultado global ranked.

5. Contexto de Aplicación

Cuándo Usarlo

  • Cuando se necesita consultar múltiples fuentes independientes para la misma solicitud.
  • Cuando las consultas a cada fuente son independientes y pueden ejecutarse en paralelo.
  • Cuando la latencia total debe ser el máximo (no la suma) de las latencias individuales.
  • Cuando se necesita una lógica de selección o combinación de los resultados de múltiples fuentes.
  • Cuando el conjunto de fuentes puede cambiar dinámicamente (nuevos proveedores se añaden, otros se retiran).

Cuándo No Usarlo

  • Cuando solo hay una fuente para consultar (no hay beneficio en scatter).
  • Cuando las consultas son dependientes entre sí (la consulta a la fuente B depende del resultado de la fuente A).
  • Cuando todas las respuestas son idénticas y no se necesita comparación ni combinación.
  • Cuando la latencia de cada fuente es despreciable y el overhead de coordinación supera al beneficio del paralelismo.

Precondiciones

  • Los destinatarios son capaces de procesar la solicitud de forma independiente.
  • Existe un mecanismo de correlación (correlation ID) para vincular respuestas con la solicitud original.
  • La lógica de agregación está bien definida (best-of-N, merge, weighted average, etc.).

Restricciones

  • El timeout del Aggregator define la ventana máxima de espera. Respuestas que llegan después del timeout se descartan.
  • El patrón consume recursos proporcionales al número de destinatarios (N mensajes en la fase scatter, N respuestas en la fase gather).
  • El Aggregator necesita estado (mantener las respuestas parciales hasta que se complete la recolección).

Tipo de Sistemas Donde Aparece con Más Frecuencia

  • Insurance quoting platforms.
  • Travel booking aggregators (Kayak, Skyscanner).
  • Distributed search engines (Elasticsearch, Solr).
  • GraphQL federation gateways (Apollo Router).
  • Financial trading systems (RFQ).
  • API gateways con aggregation (Kong, Apigee composite APIs).

6. Fuerzas Arquitectónicas

Latencia vs. Completitud

El timeout del Aggregator define el trade-off fundamental: un timeout corto minimiza la latencia pero puede producir resultados parciales (no todos los proveedores respondieron). Un timeout largo mejora la completitud pero degrada la latencia. No existe un valor universalmente correcto — depende del dominio y del SLA.

Paralelismo vs. Carga

El scatter genera carga proporcional al número de destinatarios. Si se envía la solicitud a 50 proveedores simultáneamente, se generan 50 mensajes de request y potencialmente 50 respuestas. Esto puede saturar destinatarios o la red. Un throttle en la fase de scatter puede ser necesario.

Simplicidad vs. Robustez

Una implementación simple (scatter, esperar timeout, agregar lo que haya llegado) es fácil de construir pero frágil. Una implementación robusta maneja: respuestas duplicadas, respuestas tardías (post-timeout), errores individuales de destinatarios, destinatarios que nunca responden, y respuestas con formato inesperado.

Acoplamiento vs. Flexibilidad

La variante con Recipient List acopla el scatter a una lista conocida de destinatarios. La variante con Publish-Subscribe desacopla completamente (cualquier suscriptor puede participar) pero pierde control sobre cuántas respuestas esperar.

Consistencia vs. Disponibilidad

Si se requiere que TODAS las fuentes respondan (consistencia total), la disponibilidad del sistema depende de la disponibilidad de la fuente menos confiable. Si se acepta resultados parciales, la disponibilidad mejora pero la calidad del resultado puede degradarse.


7. Estructura Conceptual del Patrón

Actores o Componentes Involucrados

  1. Requester (Solicitante): el componente que origina la solicitud que se desea dispersar.
  2. Scatter Component (Dispersor): recibe la solicitud y la envía a múltiples destinatarios. Puede ser un Recipient List o un Publish-Subscribe Channel.
  3. Recipients (Destinatarios): los N proveedores o servicios que procesan la solicitud y generan una respuesta.
  4. Gatherer / Aggregator (Recolector): recolecta las respuestas de los destinatarios, aplica la lógica de agregación, y produce el resultado consolidado.
  5. Result Consumer (Consumidor del Resultado): el componente que recibe el resultado consolidado.

Flujo Lógico

flowchart LR
    A([Requester]) --> B[(Request Channel)]
    B --> C[Scatter]
    C --> D[Recipient 1]
    C --> E[Recipient 2]
    C --> F[Recipient N]
    D --> G[(Reply Channel)]
    E --> G
    F --> G
    G --> H[Gatherer\nagregar respuestas]
    H --> I[Resultado\nAgregado]
    I --> J([Result Consumer])

Responsabilidades

Componente Responsabilidad
Requester Originar la solicitud con un correlation ID único
Scatter Difundir la solicitud a todos los destinatarios en paralelo
Recipients Procesar la solicitud y generar una respuesta (con el correlation ID)
Gatherer Recolectar respuestas, gestionar timeout, aplicar lógica de agregación
Result Consumer Consumir el resultado consolidado

Decisiones de Diseño Clave

  1. Completion condition: ¿cuándo se considera completa la recolección? Opciones: cuando llegan todas las respuestas esperadas, cuando llega un número mínimo, cuando expira el timeout, o cuando se recibe una respuesta que satisface un criterio de calidad (early termination).

  2. Aggregation strategy: ¿cómo se combinan las respuestas? Best-of-N (seleccionar la mejor según un criterio), merge (combinar todas), first-wins (la primera respuesta gana), weighted average (promedio ponderado según confiabilidad de la fuente).

  3. Timeout strategy: timeout global (una ventana única para todas las respuestas) vs. timeout per-recipient (cada destinatario tiene su propio timeout). El timeout global es más simple; el per-recipient permite tratar diferentemente a destinatarios lentos vs. rápidos.

  4. Error handling: ¿una respuesta de error cuenta como respuesta? ¿Se descarta? ¿Se reintenta al destinatario? ¿Se marca como degraded result?


8. Ejemplo Arquitectónico Detallado

Dominio: Seguros — Plataforma de Cotización Multi-Underwriter

Contexto del Negocio

Una plataforma de seguros digitales permite a brokers solicitar cotizaciones de seguro de hogar. Cuando un broker envía una solicitud de cotización, la plataforma debe consultar a múltiples underwriters (compañías aseguradoras) simultáneamente para obtener las mejores ofertas posibles. Cada underwriter evalúa el riesgo de forma independiente y devuelve una prima, coberturas y condiciones.

La plataforma trabaja con 8 underwriters integrados. El SLA del broker requiere que la respuesta esté disponible en máximo 3 segundos. Cada underwriter tiene una latencia de respuesta que varía entre 200ms y 2.5 segundos, y una disponibilidad que varía entre 95% y 99.9%.

Necesidad de Integración

La plataforma debe:

  1. Recibir la solicitud de cotización del broker (datos del inmueble, valor, ubicación, historial de siniestros).
  2. Normalizar la solicitud al formato de cada underwriter (cada uno tiene su propio API y formato).
  3. Enviar la solicitud a los 8 underwriters en paralelo.
  4. Esperar las respuestas con un timeout de 2.5 segundos.
  5. Agregar las respuestas recibidas, descartando errores y respuestas incompletas.
  6. Rankear las ofertas por precio y cobertura.
  7. Devolver al broker las top-5 ofertas.

Sistemas Involucrados

  1. Broker Portal: aplicación web donde el broker ingresa los datos de la solicitud.
  2. Quote Orchestrator: servicio que implementa el Scatter-Gather.
  3. Underwriter Adapters: 8 adaptadores, uno por underwriter, que traducen la solicitud al formato específico de cada API.
  4. Underwriter APIs: APIs externas de cada compañía aseguradora.
  5. Quote Database: almacén de cotizaciones para historial y auditoría.

Diseño del Scatter-Gather

Broker Portal → [quote.request] → Quote Orchestrator (Scatter)
                                     ├── → Adapter Allianz → Allianz API → [quote.reply] ──┐
                                     ├── → Adapter AXA → AXA API → [quote.reply] ──────────┤
                                     ├── → Adapter Zurich → Zurich API → [quote.reply] ────┤
                                     ├── → Adapter Mapfre → Mapfre API → [quote.reply] ────┤
                                     ├── → Adapter Liberty → Liberty API → [quote.reply] ──┤
                                     ├── → Adapter Generali → Generali API → [quote.reply] ┤
                                     ├── → Adapter Chubb → Chubb API → [quote.reply] ──────┤
                                     └── → Adapter HDI → HDI API → [quote.reply] ──────────┤
                                                                            Quote Aggregator (Gather)
                                                                                   [quote.response]
                                                                                     Broker Portal

Decisiones Arquitectónicas

  1. Timeout global de 2.5s: se acepta resultado parcial. Si al menos 3 underwriters han respondido al expirar el timeout, se devuelve el resultado. Si menos de 3, se extiende 500ms adicionales (grace period).

  2. Adaptadores como componentes independientes: cada adapter es un microservicio que conoce el API y formato del underwriter correspondiente. Añadir un nuevo underwriter es desplegar un nuevo adapter, no modificar el orchestrator.

  3. Correlation por quote_request_id: cada solicitud tiene un ID único. Todas las respuestas incluyen este ID para correlacionar con la solicitud original.

  4. Aggregation strategy: best-of-N ranking: las respuestas se rankean por un score compuesto de precio (60%), amplitud de cobertura (30%) y rating del underwriter (10%).

  5. Circuit breaker per adapter: si un underwriter falla consistentemente (>50% de errores en los últimos 5 minutos), su adapter se desactiva temporalmente y no participa en el scatter. Esto evita desperdiciar tiempo esperando respuestas que no llegarán.


9. Desarrollo Paso a Paso del Ejemplo

Paso 1: Recepción de la Solicitud

El broker envía una solicitud de cotización de seguro de hogar:

{
  "quote_request_id": "QR-2026-04-07-001847",
  "broker_id": "BRK-4521",
  "property": {
    "type": "apartment",
    "address": "Calle Gran Vía 45, 3B, Madrid 28013",
    "year_built": 1965,
    "area_sqm": 120,
    "estimated_value_eur": 350000,
    "construction": "brick",
    "floors": 1,
    "has_alarm": true,
    "has_reinforced_door": true
  },
  "owner": {
    "age": 42,
    "claims_last_5_years": 0,
    "years_as_customer": 8
  },
  "coverage_requested": ["fire", "water_damage", "theft", "liability", "natural_disasters"],
  "deductible_preference": "medium"
}

Paso 2: Scatter — Dispersión a Underwriters

El Quote Orchestrator recibe la solicitud, genera el correlation ID QR-2026-04-07-001847, determina los underwriters activos (6 de 8, porque Chubb y HDI tienen el circuit breaker abierto), y envía la solicitud a los 6 adaptadores en paralelo.

Cada adaptador transforma la solicitud al formato específico del underwriter. Por ejemplo, Allianz espera XML, AXA espera JSON con campos en francés, Mapfre espera JSON con su propio esquema.

Paso 3: Procesamiento por Underwriters

Los 6 underwriters procesan la solicitud en paralelo:

Underwriter Latencia Resultado
Allianz 380ms Cotización: EUR 485/año, cobertura completa
AXA 1200ms Cotización: EUR 520/año, cobertura completa + asistencia
Zurich 890ms Cotización: EUR 460/año, cobertura sin natural_disasters
Mapfre 450ms Cotización: EUR 440/año, cobertura completa
Liberty 2800ms (excede timeout de 2500ms)
Generali Error 503 Service Unavailable

Paso 4: Gather — Recolección y Agregación

El Aggregator opera con las siguientes reglas:

  1. T=380ms: llega respuesta de Allianz. Almacena. Respuestas recolectadas: 1/6.
  2. T=450ms: llega respuesta de Mapfre. Almacena. Respuestas: 2/6.
  3. T=890ms: llega respuesta de Zurich. Almacena. Respuestas: 3/6. Mínimo alcanzado.
  4. T=1200ms: llega respuesta de AXA. Almacena. Respuestas: 4/6.
  5. T=1200ms: llega error de Generali. Registra error, no cuenta como respuesta válida. Respuestas válidas: 4/6.
  6. T=2500ms: timeout expira. Liberty no respondió. Se procede con 4 respuestas válidas.

Paso 5: Ranking y Resultado

El Aggregator aplica el scoring:

Underwriter Precio Cobertura Score Rating Score Total Ranking
Mapfre 440 100% 8.5 92.5 1
Allianz 485 100% 9.2 88.3 2
Zurich 460 80% (sin natural_disasters) 9.0 82.1 3
AXA 520 110% (con asistencia) 8.8 81.7 4

Paso 6: Respuesta al Broker

{
  "quote_request_id": "QR-2026-04-07-001847",
  "status": "PARTIAL",
  "quotes_requested": 6,
  "quotes_received": 4,
  "quotes_failed": 1,
  "quotes_timeout": 1,
  "elapsed_ms": 2500,
  "ranked_quotes": [
    { "rank": 1, "underwriter": "Mapfre", "annual_premium_eur": 440, "coverage": "full", "score": 92.5 },
    { "rank": 2, "underwriter": "Allianz", "annual_premium_eur": 485, "coverage": "full", "score": 88.3 },
    { "rank": 3, "underwriter": "Zurich", "annual_premium_eur": 460, "coverage": "partial", "score": 82.1 },
    { "rank": 4, "underwriter": "AXA", "annual_premium_eur": 520, "coverage": "full+extras", "score": 81.7 }
  ]
}

El broker ve las 4 ofertas rankeadas y puede presentar las opciones al cliente. El status PARTIAL indica que no todos los underwriters respondieron — el broker puede decidir si esperar a un re-quote posterior o proceder con las ofertas disponibles.


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 Kafka
from diagrams.onprem.compute import Server
from diagrams.onprem.client import User
from diagrams.onprem.database import PostgreSQL
from diagrams.generic.blank import Blank

with Diagram("Scatter-Gather - Insurance Quote Platform", show=False, direction="TB"):

    broker = User("Broker\nPortal")

    with Cluster("Quote Orchestrator"):
        scatter = Server("Scatter\n(Recipient List)")
        gather = Server("Gather\n(Aggregator)")

    with Cluster("Underwriter Adapters"):
        allianz = Server("Adapter\nAllianz")
        axa = Server("Adapter\nAXA")
        zurich = Server("Adapter\nZurich")
        mapfre = Server("Adapter\nMapfre")
        liberty = Server("Adapter\nLiberty")
        generali = Server("Adapter\nGenerali")

    reply_q = Kafka("quote.replies")
    db = PostgreSQL("Quote\nDB")

    broker >> Edge(label="quote request") >> scatter

    scatter >> Edge(label="scatter") >> allianz
    scatter >> Edge(label="scatter") >> axa
    scatter >> Edge(label="scatter") >> zurich
    scatter >> Edge(label="scatter") >> mapfre
    scatter >> Edge(label="scatter") >> liberty
    scatter >> Edge(label="scatter") >> generali

    allianz >> Edge(label="reply") >> reply_q
    axa >> Edge(label="reply") >> reply_q
    zurich >> Edge(label="reply") >> reply_q
    mapfre >> Edge(label="reply") >> reply_q
    liberty >> Edge(label="timeout", style="dashed", color="red") >> reply_q
    generali >> Edge(label="error", style="dashed", color="red") >> reply_q

    reply_q >> Edge(label="aggregate") >> gather
    gather >> Edge(label="ranked result") >> broker
    gather >> Edge(label="persist") >> db
from diagrams import Diagram, Cluster, Edge
from diagrams.onprem.client import User
from diagrams.aws.compute import Lambda
from diagrams.aws.database import Dynamodb
from diagrams.aws.integration import SQS, StepFunctions


with Diagram("Scatter-Gather - Insurance Quote Platform (AWS)", show=False, direction="TB"):

    broker = User("Broker\nPortal")

    with Cluster("Quote Orchestrator (Step Functions)"):
        scatter_gather = StepFunctions("Step Functions\nParallel State\n(Scatter + Gather\n+ Timeout)")

    with Cluster("Underwriter Adapters"):
        allianz = Lambda("Adapter\nAllianz")
        axa = Lambda("Adapter\nAXA")
        zurich = Lambda("Adapter\nZurich")
        mapfre = Lambda("Adapter\nMapfre")
        liberty = Lambda("Adapter\nLiberty")
        generali = Lambda("Adapter\nGenerali")

    reply_q = SQS("quote.replies")
    db = Dynamodb("Dynamodb\nQuote Store")

    broker >> Edge(label="quote request") >> scatter_gather

    scatter_gather >> Edge(label="scatter") >> allianz
    scatter_gather >> Edge(label="scatter") >> axa
    scatter_gather >> Edge(label="scatter") >> zurich
    scatter_gather >> Edge(label="scatter") >> mapfre
    scatter_gather >> Edge(label="scatter") >> liberty
    scatter_gather >> Edge(label="scatter") >> generali

    allianz >> Edge(label="reply") >> reply_q
    axa >> Edge(label="reply") >> reply_q
    zurich >> Edge(label="reply") >> reply_q
    mapfre >> Edge(label="reply") >> reply_q
    liberty >> Edge(label="timeout", style="dashed", color="red") >> reply_q
    generali >> Edge(label="error", style="dashed", color="red") >> reply_q

    reply_q >> Edge(label="aggregate") >> scatter_gather
    scatter_gather >> Edge(label="ranked result") >> broker
    scatter_gather >> Edge(label="persist") >> db
from diagrams import Diagram, Cluster, Edge
from diagrams.onprem.client import User
from diagrams.azure.compute import FunctionApps
from diagrams.azure.database import CosmosDb
from diagrams.azure.devops import ApplicationInsights


with Diagram("Scatter-Gather - Insurance Quote Platform (Azure)", show=False, direction="TB"):

    broker = User("Broker\nPortal")

    with Cluster("Durable Functions Orchestrator (Fan-Out / Fan-In)"):
        orchestrator = FunctionApps("Quote\nOrchestrator\n(Durable Function)")

    with Cluster("Activity Functions (Parallel Adapters)"):
        allianz = FunctionApps("Adapter\nAllianz")
        axa = FunctionApps("Adapter\nAXA")
        zurich = FunctionApps("Adapter\nZurich")
        mapfre = FunctionApps("Adapter\nMapfre")
        liberty = FunctionApps("Adapter\nLiberty")
        generali = FunctionApps("Adapter\nGenerali")

    db = CosmosDb("Cosmos DB\n(Quotes)")
    monitoring = ApplicationInsights("Application\nInsights")

    broker >> Edge(label="quote request\n(HTTP trigger)") >> orchestrator

    # Fan-out: orchestrator calls all activities in parallel
    orchestrator >> Edge(label="CallActivityAsync") >> allianz
    orchestrator >> Edge(label="CallActivityAsync") >> axa
    orchestrator >> Edge(label="CallActivityAsync") >> zurich
    orchestrator >> Edge(label="CallActivityAsync") >> mapfre
    orchestrator >> Edge(label="CallActivityAsync") >> liberty
    orchestrator >> Edge(label="CallActivityAsync") >> generali

    # Fan-in: Task.WhenAll aggregates results
    allianz >> Edge(label="quote") >> orchestrator
    axa >> Edge(label="quote") >> orchestrator
    zurich >> Edge(label="quote") >> orchestrator
    mapfre >> Edge(label="quote") >> orchestrator
    liberty >> Edge(label="timeout", style="dashed", color="red") >> orchestrator
    generali >> Edge(label="error", style="dashed", color="red") >> orchestrator

    orchestrator >> Edge(label="ranked result") >> broker
    orchestrator >> Edge(label="persist") >> db
    orchestrator >> Edge(style="dotted") >> monitoring

Notas del Diagrama

  • Las flechas sólidas representan el flujo normal (scatter y reply exitoso).
  • Las flechas punteadas rojas representan respuestas degradadas (timeout de Liberty, error de Generali).
  • El Aggregator recolecta de un canal de replies compartido, correlacionando por quote_request_id.
  • La persistencia en Quote DB es asíncrona y no afecta la latencia de respuesta al broker.

11. Beneficios

Minimización de Latencia

La latencia total del Scatter-Gather es el máximo de las latencias individuales (más overhead de coordinación), no la suma. Para 6 fuentes con latencias de 380ms a 2800ms y un timeout de 2500ms, la latencia total es 2500ms — no los 5720ms que tomaría una consulta secuencial.

Resiliencia ante Fallos Parciales

El patrón tolera fallos individuales de destinatarios. Si 2 de 6 underwriters fallan, el resultado incluye 4 cotizaciones válidas en lugar de fallar completamente. La calidad del resultado se degrada gracefully en lugar de fallar catastróficamente.

Desacoplamiento de Proveedores

Los destinatarios son independientes entre sí y del orchestrator. Añadir un nuevo underwriter es desplegar un nuevo adapter, no modificar la lógica de scatter ni de gather. Eliminar un underwriter es retirar su adapter.

Comparabilidad y Optimización

El patrón permite comparar respuestas de múltiples fuentes y seleccionar la óptima según criterios definidos. Sin scatter-gather, la selección del mejor proveedor requiere consulta manual o asignación estática.

Transparencia Operativa

El Aggregator proporciona visibilidad completa: cuántos destinatarios fueron consultados, cuántos respondieron, cuántos fallaron, cuánto tardó cada uno. Esto habilita métricas de calidad de servicio por proveedor.


12. Desventajas y Riesgos

Complejidad del Aggregator

El Aggregator es stateful: debe mantener el estado de cada solicitud scatter (qué respuestas se esperan, cuáles han llegado, el timeout). Esto introduce complejidad de gestión de estado, especialmente en entornos distribuidos donde el Aggregator puede tener múltiples instancias.

Carga Amplificada

Cada solicitud genera N mensajes de request y potencialmente N respuestas. Si el sistema procesa 1000 solicitudes/segundo y cada scatter tiene 8 destinatarios, se generan 8000 requests/segundo a los destinatarios. Esta amplificación puede saturar destinatarios que no están dimensionados para esa carga.

Timeout Tuning

El timeout es un parámetro crítico que requiere tuning continuo. Un timeout demasiado corto descarta respuestas útiles que llegan ligeramente tarde. Un timeout demasiado largo degrada la experiencia del usuario que espera el resultado.

Resultados Parciales Ambiguos

Cuando el resultado es parcial (no todos los destinatarios respondieron), el consumidor debe interpretar correctamente la completitud del resultado. ¿Es la mejor oferta realmente la mejor, o el underwriter más competitivo fue el que no respondió?

Costo de Infraestructura

El patrón requiere infraestructura para: canales de reply, gestión de correlation, almacenamiento temporal de respuestas parciales, timers para timeout. Esto es significativamente más complejo que una consulta point-to-point.

Respuestas Tardías (Stale Replies)

Respuestas que llegan después del timeout consumen recursos (red, procesamiento) pero se descartan. Si un destinatario es consistentemente lento, genera carga sin contribuir al resultado. Se necesita un mecanismo para detectar y desactivar destinatarios crónicamente lentos.


13. Relación con Otros Patrones

Patrones que Componen el Scatter-Gather

  • Recipient List: implementa la fase de scatter cuando los destinatarios son conocidos. El Scatter-Gather usa un Recipient List para difundir la solicitud a N destinatarios específicos.
  • Publish-Subscribe Channel: implementa la fase de scatter cuando los destinatarios son dinámicos (variante auction). Cualquier suscriptor puede participar.
  • Aggregator: implementa la fase de gather. Recolecta las N respuestas, aplica la condición de completitud y la estrategia de agregación.

Patrones Relacionados

  • Composed Message Processor: similar en estructura (split → process → aggregate) pero procesa partes de un mismo mensaje, no consulta múltiples fuentes independientes.
  • Request-Reply: cada par scatter-destinatario-reply es una instancia de Request-Reply. El Scatter-Gather es una composición de N Request-Reply paralelos con agregación.
  • Competing Consumers: los destinatarios del scatter NO son Competing Consumers (cada uno recibe la solicitud). Pero múltiples instancias del Aggregator SÍ podrían ser Competing Consumers si se particionan por correlation ID.
  • Process Manager: puede usar Scatter-Gather como un paso dentro de un flujo más complejo. Por ejemplo, un Process Manager que orquesta la emisión de una póliza puede incluir un paso de Scatter-Gather para la cotización.
  • Content-Based Router: después del gather, un Content-Based Router puede enrutar el resultado según criterios del resultado agregado (por ejemplo, enrutar a un canal de aprobación manual si ninguna cotización cumple el presupuesto).

14. Relevancia Actual

Vigencia del Patrón

ALTA. Scatter-Gather es más relevante que nunca en arquitecturas modernas por varias razones:

  1. Microservices: la consulta a múltiples servicios en paralelo es un patrón cotidiano. Un API gateway que necesita datos de 5 microservicios para componer una respuesta está ejecutando un Scatter-Gather.

  2. GraphQL Federation: Apollo Federation Router ejecuta un Scatter-Gather puro — recibe una query GraphQL, la descompone en subqueries para diferentes subgraphs, las ejecuta en paralelo, y agrega los resultados en una respuesta unificada.

  3. Distributed databases: Elasticsearch, Cassandra, MongoDB (con sharding) ejecutan Scatter-Gather internamente para queries distribuidas. El coordinador scatter la query a todos los shards relevantes y gather los resultados parciales.

  4. Cloud-native fan-out: AWS Step Functions Parallel state, Azure Durable Functions fan-out/fan-in, y Temporal parallel activities son implementaciones directas de Scatter-Gather.

Evolución del Patrón

El patrón ha evolucionado desde su definición original (principalmente messaging-based) hacia implementaciones más variadas:

  • Reactive streams: frameworks como Project Reactor y RxJava proporcionan operadores nativos para fan-out y merge (flatMap, zip, merge) que implementan Scatter-Gather de forma reactiva.
  • gRPC bidirectional streaming: permite scatter-gather sobre streams persistentes sin crear nuevas conexiones por solicitud.
  • Edge computing: en CDN y edge functions, scatter-gather se usa para consultar múltiples regiones o caches en paralelo.

15. Implementación Moderna

Con Apache Kafka y Spring Cloud Stream

@Service
public class QuoteScatterGather {

    private final KafkaTemplate<String, QuoteRequest> kafkaTemplate;
    private final Map<String, PendingGather> pendingGathers = new ConcurrentHashMap<>();

    public CompletableFuture<AggregatedQuotes> requestQuotes(QuoteRequest request) {
        String correlationId = request.getQuoteRequestId();
        PendingGather gather = new PendingGather(6, Duration.ofMillis(2500));
        pendingGathers.put(correlationId, gather);

        // Scatter phase
        List<String> adapters = List.of(
            "quote.allianz", "quote.axa", "quote.zurich",
            "quote.mapfre", "quote.liberty", "quote.generali"
        );
        for (String topic : adapters) {
            kafkaTemplate.send(topic, correlationId, request);
        }

        // Gather phase (timeout-aware)
        return gather.getFuture();
    }

    @KafkaListener(topics = "quote.replies", groupId = "quote-aggregator")
    public void onQuoteReply(QuoteReply reply) {
        PendingGather gather = pendingGathers.get(reply.getCorrelationId());
        if (gather != null) {
            gather.addReply(reply);
        }
        // Replies after timeout are silently discarded
    }
}

Con AWS Step Functions (Parallel State)

{
  "Type": "Parallel",
  "Branches": [
    {
      "StartAt": "QuoteAllianz",
      "States": {
        "QuoteAllianz": {
          "Type": "Task",
          "Resource": "arn:aws:lambda:eu-west-1:123:function:quote-allianz",
          "TimeoutSeconds": 3,
          "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "AllianzFailed" }],
          "End": true
        },
        "AllianzFailed": { "Type": "Pass", "Result": { "status": "FAILED" }, "End": true }
      }
    },
    {
      "StartAt": "QuoteAXA",
      "States": {
        "QuoteAXA": {
          "Type": "Task",
          "Resource": "arn:aws:lambda:eu-west-1:123:function:quote-axa",
          "TimeoutSeconds": 3,
          "Catch": [{ "ErrorEquals": ["States.ALL"], "Next": "AXAFailed" }],
          "End": true
        },
        "AXAFailed": { "Type": "Pass", "Result": { "status": "FAILED" }, "End": true }
      }
    }
  ],
  "Next": "AggregateQuotes"
}

Con Temporal (Go)

func QuoteWorkflow(ctx workflow.Context, req QuoteRequest) (AggregatedQuotes, error) {
    // Scatter: launch all quote activities in parallel
    var futures []workflow.Future
    underwriters := []string{"allianz", "axa", "zurich", "mapfre", "liberty", "generali"}

    actOpts := workflow.ActivityOptions{
        StartToCloseTimeout: 3 * time.Second,
    }
    actCtx := workflow.WithActivityOptions(ctx, actOpts)

    for _, uw := range underwriters {
        future := workflow.ExecuteActivity(actCtx, GetQuoteActivity, req, uw)
        futures = append(futures, future)
    }

    // Gather: collect results, tolerating individual failures
    var quotes []Quote
    for i, future := range futures {
        var quote Quote
        err := future.Get(ctx, &quote)
        if err != nil {
            // Log failure but continue gathering
            workflow.GetLogger(ctx).Warn("Quote failed",
                "underwriter", underwriters[i], "error", err)
            continue
        }
        quotes = append(quotes, quote)
    }

    return AggregateAndRank(quotes), nil
}

Con Kotlin Coroutines (Reactive)

suspend fun scatterGatherQuotes(request: QuoteRequest): AggregatedQuotes = coroutineScope {
    val adapters = listOf(allianzAdapter, axaAdapter, zurichAdapter, mapfreAdapter)

    val deferredQuotes = adapters.map { adapter ->
        async {
            try {
                withTimeout(2500) { adapter.getQuote(request) }
            } catch (e: Exception) {
                null  // Timeout or error → null (filtered later)
            }
        }
    }

    val quotes = deferredQuotes.awaitAll().filterNotNull()
    AggregatedQuotes(
        quotes = quotes.sortedBy { it.score }.reversed(),
        totalRequested = adapters.size,
        totalReceived = quotes.size
    )
}

16. Gobierno y Operación

Métricas Clave

Métrica Descripción Umbral de Alerta
scatter.recipients.count Número de destinatarios activos por scatter <3 (pocos proveedores)
gather.completion.rate % de scatters con todas las respuestas <80%
gather.timeout.rate % de respuestas que exceden timeout >20% por destinatario
gather.latency.p99 Latencia P99 del gather completo >SLA
recipient.error.rate Tasa de error por destinatario >10%
recipient.latency.p95 Latencia P95 por destinatario >timeout
partial.result.rate % de resultados parciales devueltos >30%

Circuit Breaker por Destinatario

Cada destinatario debe tener un circuit breaker independiente que se abre cuando: - La tasa de error supera el 50% en los últimos 5 minutos. - La latencia P95 supera consistentemente el timeout. - El destinatario no responde en 10 solicitudes consecutivas.

Cuando un circuit breaker está abierto, el destinatario se excluye del scatter, reduciendo la carga innecesaria y evitando esperar respuestas que no llegarán.

Alerting

  • Alerta crítica: completition rate < 50% (más de la mitad de los scatters devuelven resultados muy parciales).
  • Alerta warning: un destinatario tiene circuit breaker abierto por más de 30 minutos.
  • Alerta info: se añade o elimina un destinatario dinámicamente.

Operaciones de Runbook

  1. Destinatario consistentemente lento: revisar SLA del destinatario, ajustar timeout, considerar desactivación temporal.
  2. Agregador con acumulación de estado: verificar que las entradas de gather se limpian después del timeout (leak de memoria si no se limpian).
  3. Respuestas huérfanas: monitorear respuestas que llegan para correlation IDs que ya expiraron. Si son frecuentes, el timeout es demasiado agresivo.

17. Errores Comunes

Error 1: No Implementar Timeout

Esperar indefinidamente a que todos los destinatarios respondan. Si un destinatario está caído, el scatter-gather nunca completa. Solución: siempre implementar un timeout global con política clara de resultados parciales.

Error 2: Timeout Demasiado Generoso

Configurar un timeout tan alto que la latencia del scatter-gather es peor que consultar secuencialmente. Si el timeout es 10 segundos pero las respuestas típicas llegan en 200-500ms, el usuario espera innecesariamente cuando un destinatario falla. Solución: timeout basado en P95 de latencia de los destinatarios, no en el peor caso teórico.

Error 3: No Manejar Resultados Parciales

Asumir que siempre se recibirán todas las respuestas. Cuando el resultado es parcial, el consumidor debe saber que no tiene la imagen completa. Solución: incluir metadatos de completitud en el resultado (N solicitados, N recibidos, N fallidos).

Error 4: Aggregator Sin Limpieza de Estado

El Aggregator almacena estado por cada scatter (respuestas parciales pendientes). Si no se limpia este estado después del timeout, el Aggregator sufre memory leak progresivo. Solución: cleanup automático de estado vencido con un scheduler o TTL en el almacén de estado.

Error 5: Confundir Scatter-Gather con Broadcast

Un broadcast (Publish-Subscribe) envía un mensaje a todos los suscriptores sin esperar respuesta. Scatter-Gather espera y agrega respuestas. Usar broadcast cuando se necesita aggregation deja sin mecanismo de recolección. Solución: usar explícitamente un Aggregator como componente de gather.

Error 6: Ignorar la Carga Amplificada

No dimensionar los destinatarios para la carga multiplicada del scatter. Si el sistema recibe 100 req/s y hay 10 destinatarios, cada destinatario recibe 100 req/s, no 10. Solución: capacity planning considerando la amplificación del scatter.

Error 7: No Implementar Correlation

Sin correlation ID, las respuestas de múltiples scatters concurrentes se mezclan. El Aggregator no puede determinar qué respuesta corresponde a qué solicitud. Solución: correlation ID obligatorio en cada solicitud y respuesta.


18. Conclusión Técnica

Scatter-Gather es un patrón de enrutamiento compuesto que resuelve elegantemente el problema de consulta paralela a múltiples fuentes con agregación de resultados. Su valor principal es la reducción de latencia (de suma a máximo de latencias individuales) y la resiliencia ante fallos parciales de destinatarios.

El patrón es una composición de dos patrones más simples — Recipient List (o Publish-Subscribe) para el scatter y Aggregator para el gather — pero su valor como patrón compuesto reside en encapsular la semántica completa de "consultar N fuentes en paralelo y combinar los resultados".

Las decisiones de diseño más críticas son el timeout (que define el trade-off entre latencia y completitud), la estrategia de agregación (best-of-N, merge, first-wins) y la política de resultados parciales (cuántas respuestas son suficientes para devolver un resultado útil).

En arquitecturas modernas, Scatter-Gather es ubicuo: GraphQL federation, distributed database queries, parallel API composition, fan-out/fan-in en workflows, y market data aggregation son todas instancias del patrón. Frameworks como Temporal, Step Functions, y operadores reactivos (flatMap + merge) proporcionan primitivas que simplifican su implementación, pero las decisiones de diseño — timeout, completion condition, error handling — siguen siendo responsabilidad del arquitecto.

La clave para una implementación exitosa es aceptar la realidad de los resultados parciales: en un sistema distribuido, no siempre se obtienen todas las respuestas. Un Scatter-Gather bien diseñado degrada gracefully, proporcionando el mejor resultado posible con las respuestas disponibles, en lugar de fallar completamente cuando un destinatario no responde.


Scatter-Gather es el patrón que convierte la consulta paralela de un problema de coordinación ad-hoc en una solución arquitectónica estandarizada, con semántica clara de timeout, correlación y agregación.