En la lliçó anterior vam veure què és un esdeveniment i qui el produeix o consumeix. Ara baixem un nivell: com viatja físicament aquest missatge d'un servei a un altre de forma fiable quan tots dos no es coneixen i poden estar arrencant, caient o saturats en moments diferents? La resposta és la missatgeria asíncrona a través de brokers de missatges. Un broker és una peça d'infraestructura intermèdia que rep missatges dels productors, els emmagatzema de forma duradora i els entrega als consumidors quan aquests estan preparats.

Dominar la missatgeria asíncrona és imprescindible perquè d'ella depenen la fiabilitat i la coherència de qualsevol sistema distribuït. En aquesta lliçó estudiarem les dues primitives bàsiques (cues i topics), les garanties d'entrega, el problema dels duplicats i com resoldre'l amb idempotència, i compararem les tres tecnologies més usades del mercat: RabbitMQ, Apache Kafka i Amazon SQS.

Contingut

  1. Per què missatgeria asíncrona?
  2. Cues (point-to-point) vs Topics (publicació/subscripció)
  3. Garanties d'entrega: at-most-once, at-least-once, exactly-once
  4. El problema dels duplicats i la idempotència
  5. Comparativa: RabbitMQ vs Kafka vs Amazon SQS
  6. Exemple pràctic: productor i consumidor idempotent
  7. Errors comuns i consells
  8. Exercicis i solucions
  9. Conclusió

  1. Per què missatgeria asíncrona?

En una crida síncrona, si el servei B està caigut, la crida d'A falla. Amb missatgeria asíncrona, A diposita el missatge al broker i continua treballant; quan B es recuperi, el consumirà. Això aporta:

  • Desacoblament temporal: productor i consumidor no necessiten estar vius al mateix temps.
  • Amortiment de pics (buffering): si arriben 10.000 missatges de cop, el broker els reté i el consumidor els processa al seu ritme.
  • Resiliència: un consumidor lent o caigut no tomba el productor.

  1. Cues (point-to-point) vs Topics (publicació/subscripció)

Existeixen dos models de distribució de missatges.

Cua (point-to-point)

Un missatge en una cua el processa un únic consumidor. Si hi ha diversos consumidors connectats a la mateixa cua, el broker reparteix (balanceja) els missatges entre ells, però cada missatge va a un de sol. Es fa servir per distribuir treball (work queue).

Topic (publicació/subscripció)

Un missatge publicat en un topic s'entrega a tots els subscriptors. Es fa servir per difondre esdeveniments a múltiples interessats.

flowchart TB
    subgraph Cola["CUA (point-to-point)"]
        P1[Productor] --> Q[(Cua)]
        Q --> CA[Consumidor A]
        Q --> CB[Consumidor B]
        Q -. cada missatge a UN de sol .-> CA
    end
    subgraph Topic["TOPIC (pub/sub)"]
        P2[Productor] --> T{{Topic}}
        T --> SA[Subscriptor A]
        T --> SB[Subscriptor B]
        T -. cada missatge a TOTS .-> SA
    end
Aspecte Cua (point-to-point) Topic (pub/sub)
Receptors per missatge Un Tots els subscriptors
Objectiu Repartir càrrega de treball Difondre esdeveniments
Patró típic Comandes / tasques Esdeveniments de domini
Exemple Cua d'enviament d'emails "ComandaCreada" a inventari, facturació, enviaments

  1. Garanties d'entrega: at-most-once, at-least-once, exactly-once

Quan un missatge viatja per la xarxa, es poden perdre acusaments de rebut (acks), caure processos, etc. Per això existeixen diferents nivells de garantia:

Garantia Significat Risc Cost
At-most-once S'entrega 0 o 1 vegada Es pot perdre un missatge Mínim (sense reintents)
At-least-once S'entrega 1 o més vegades Es pot duplicar Mitjà (requereix reintents i acks)
Exactly-once S'entrega exactament 1 vegada Cap (ideal) Alt (complex, no sempre real)

Punts clau:

  • At-most-once: el consumidor confirma abans de processar. Si falla, el missatge es perd. Només vàlid si perdre alguna dada és acceptable (p. ex. mètriques no crítiques).
  • At-least-once: el consumidor confirma després de processar amb èxit. Si falla just abans de l'ack, el missatge es torna a entregar → pot haver-hi duplicats. És el nivell més habitual i recomanat.
  • Exactly-once: semànticament perfecte, però costós. En sistemes distribuïts purs és molt difícil de garantir d'extrem a extrem; sol aconseguir-se combinant at-least-once amb idempotència en el consumidor (ho veurem a continuació). Kafka ofereix "exactly-once" dins dels seus propis límits (transaccions), però tan bon punt l'efecte surt fora de Kafka (escriure en una altra BD, cridar una API), torna a dependre de la idempotència.

Regla d'or del món real: dissenya per a at-least-once i fes els teus consumidors idempotents. Això et dona, a la pràctica, l'efecte d'exactly-once.

  1. El problema dels duplicats i la idempotència

Una operació és idempotent si executar-la diverses vegades produeix el mateix resultat que executar-la una sola vegada. Si els nostres consumidors són idempotents, els duplicats d'at-least-once deixen de ser un problema.

Tècniques habituals per aconseguir idempotència:

  1. Clau d'idempotència / ID de missatge: registrar els IDs ja processats i descartar els repetits.
  2. Operacions naturalment idempotents: UPDATE saldo SET valor = 100 (assignació absoluta) és idempotent; UPDATE saldo SET valor = valor + 100 (increment) no ho és.
  3. UPSERT amb clau única: inserir o ignorar si ja existeix.
-- Taula que registra quins missatges ja hem processat.
CREATE TABLE mensajes_procesados (
    mensaje_id  VARCHAR(64) PRIMARY KEY,  -- clau d'idempotència
    procesado_en TIMESTAMP NOT NULL
);

-- En rebre un missatge, intentem inserir el seu ID.
-- Si ja existeix (clau primària duplicada), sabem que és un duplicat.
INSERT INTO mensajes_procesados (mensaje_id, procesado_en)
VALUES ('msg-abc-123', CURRENT_TIMESTAMP)
ON CONFLICT (mensaje_id) DO NOTHING;  -- PostgreSQL: ignora si ja existeix

Explicació:

  • mensaje_id és la clau primària: la base de dades garanteix que no n'hi hagi dos d'iguals.
  • ON CONFLICT ... DO NOTHING fa que el segon intent d'inserir el mateix ID no produeixi error ni efecte. Així detectem el duplicat sense lògica addicional complexa.
  • Si l'INSERT va afectar 0 files, era un duplicat i podem saltar-nos el processament.

  1. Comparativa: RabbitMQ vs Kafka vs Amazon SQS

Característica RabbitMQ Apache Kafka Amazon SQS
Model principal Broker de cues (AMQP) Log distribuït d'esdeveniments Cua gestionada (cloud)
Paradigma Cues + exchanges (pub/sub) Topics particionats + offsets Cues (Standard i FIFO)
Retenció de missatges Fins a consumir (s'esborren) Configurable (dies/setmanes), reproduïble Fins a 14 dies
Reproducció (replay) No nativa Sí (rellegir des d'un offset) No
Ordre Per cua Per partició FIFO només en cues FIFO
Throughput Alt Molt alt (milions/s) Alt (escala automàtica)
Garantia típica At-least-once At-least-once / exactly-once* At-least-once (Std) / exactly-once (FIFO)
Gestió Autogestionat / cloud Autogestionat / gestionat Totalment gestionat (AWS)
Cas ideal Enrutament complex, RPC, tasques Streaming, event sourcing, big data Desacoblament simple a AWS sense operar infra

Resum pràctic:

  • RabbitMQ: excel·lent quan necessites enrutament flexible (exchanges amb regles) i patrons tradicionals de cues de treball.
  • Kafka: l'elecció per a alt volum, retenció i reproducció d'esdeveniments; base d'event sourcing i streaming (lliçó 05-05).
  • SQS: el més simple si ja ets a AWS i només vols desacoblar sense gestionar servidors.

  1. Exemple pràctic: productor i consumidor idempotent

Vegem un consumidor de Kafka en Spring que aplica at-least-once + idempotència.

@Component
public class ConsumidorPagos {

    private final RepositorioIdempotencia idempotencia;
    private final ServicioContabilidad contabilidad;

    public ConsumidorPagos(RepositorioIdempotencia idempotencia,
                           ServicioContabilidad contabilidad) {
        this.idempotencia = idempotencia;
        this.contabilidad = contabilidad;
    }

    @KafkaListener(topics = "pagos.confirmados", groupId = "contabilidad")
    public void consumir(PagoConfirmadoEvent evento, Acknowledgment ack) {
        // 1. Ja hem processat aquest missatge? -> idempotència
        if (!idempotencia.registrarSiEsNuevo(evento.pagoId())) {
            ack.acknowledge(); // duplicat: confirmem i sortim
            return;
        }
        // 2. Lògica de negoci real
        contabilidad.asentarApunte(evento);
        // 3. Confirmem NOMÉS després de processar amb èxit (at-least-once)
        ack.acknowledge();
    }
}

Explicació detallada:

  • @KafkaListener subscriu el mètode al topic pagos.confirmados. El groupId "contabilidad" identifica aquest grup de consumidors; Kafka reparteix les particions entre els membres del grup.
  • registrarSiEsNuevo(...) intenta inserir l'ID (com en el SQL anterior). Retorna false si ja existia → és un duplicat, el confirmem i sortim sense reprocessar.
  • La confirmació (ack.acknowledge()) es fa després d'asentarApunte. Si el procés mor abans de l'ack, Kafka tornarà a entregar el missatge (at-least-once), però la idempotència evitarà el doble assentament.
# Configuració Spring Kafka per a confirmació manual (clau de l'at-least-once)
spring:
  kafka:
    consumer:
      group-id: contabilidad
      enable-auto-commit: false      # NO confirmar automàticament
      auto-offset-reset: earliest    # llegir des del principi si no hi ha offset
    listener:
      ack-mode: manual               # confirmem nosaltres amb ack.acknowledge()
  • enable-auto-commit: false és essencial: si Kafka confirmés sol, podria confirmar abans que acabéssim de processar i perdríem missatges davant d'una fallada.
  • ack-mode: manual delega en el nostre codi el moment exacte de la confirmació.

Errors Comuns i Consells

  • Confirmar abans de processar. Converteix el teu at-least-once en at-most-once accidental i perds missatges davant de fallades. Confirma sempre al final.
  • Creure que "exactly-once" elimina la necessitat d'idempotència. Tan bon punt l'efecte creua la frontera del broker (una altra BD, una API externa), necessites idempotència igualment.
  • No dimensionar la dead-letter queue (DLQ). Els missatges que fallen repetidament han d'anar a una cua de descart per no bloquejar la cua principal en un bucle infinit de reintents.
  • Fer servir topic quan volies cua (o a la inversa). Difondre una comanda a "tots" pot executar la mateixa acció N vegades.
  • Consell: defineix sempre un camp mensaje_id únic en els teus esdeveniments des del primer dia; afegir-lo després és dolorós.

Exercicis

  1. Un equip processa pagaments amb un consumidor que confirma el missatge just en rebre'l i després contacta amb la passarel·la bancària. Si el procés mor entre l'ack i la crida bancària, quina garantia real tenen i quin problema passa? Com ho corregiries?
  2. Indica per a cada cas si faries servir cua o topic: (a) enviar el correu de benvinguda una sola vegada; (b) notificar a inventari, facturació i CRM d'una nova comanda; (c) repartir 1.000 tasques de generació de PDF entre 5 workers.
  3. Escriu una sentència SQL idempotent per "marcar una comanda com a pagada" que pugui executar-se diverses vegades sense efectes secundaris.

Solucions

  1. Tenen at-most-once: si mor després de l'ack, el missatge no es torna a entregar i el pagament mai no arriba a la passarel·la (es perd). Correcció: confirmar després de la crida bancària (at-least-once) i fer l'operació idempotent amb la clau d'idempotència del pagament per no cobrar dues vegades davant d'un reintent.
  2. (a) Cua (un sol receptor, una sola vegada). (b) Topic (tres subscriptors reben l'esdeveniment). (c) Cua (repartiment de treball entre workers).
  3. Per exemple:
-- Assignació absoluta de l'estat: idempotent.
UPDATE pedidos
SET estado = 'PAGADO', pagado_en = COALESCE(pagado_en, CURRENT_TIMESTAMP)
WHERE pedido_id = :pedidoId AND estado <> 'PAGADO';

Executar-la de nou no canvia res perquè la condició estado <> 'PAGADO' ja no es compleix i pagado_en es conserva amb COALESCE.

Conclusió

La missatgeria asíncrona és el sistema circulatori de les arquitectures dirigides per esdeveniments. Vam diferenciar cues (un receptor, repartiment de càrrega) de topics (tots els subscriptors, difusió). Vam entendre les tres garanties d'entrega i per què at-least-once + idempotència és la combinació pragmàtica que fa servir la indústria. Finalment, vam comparar RabbitMQ, Kafka i SQS per saber quan triar cadascuna.

En la lliçó següent, "Patrons d'Esdeveniments: Event Sourcing i CQRS", veurem com, en lloc de desar només l'estat actual, podem emmagatzemar la seqüència completa d'esdeveniments com a font de veritat, i com separar els models de lectura i escriptura per escalar.

Curs d'Arquitectura d'Aplicacions

Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions

Mòdul 2: Principis i Tàctiques de Disseny

Mòdul 3: Estils i Patrons Arquitectònics

Mòdul 4: Arquitectures Distribuïdes i Microserveis

Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria

Mòdul 6: Disseny Dirigit pel Domini (DDD)

Mòdul 7: Dades i Persistència

Mòdul 8: Arquitectura al Núvol i Desplegament

Mòdul 9: Qualitat, Seguretat i Observabilitat

Mòdul 10: Evolució, Governança i Casos Pràctics

© Copyright 2026. Tots els drets reservats