A la lliçó anterior vam fixar els contractes REST de TechCorp, però vam deixar clar que les crides síncrones serien poques: Comandes → Catàleg i Comandes → Clients. La resta del flux "un client fa una comanda" (reservar estoc, cobrar, confirmar, notificar) el vam dissenyar a 02-05 com una saga per coreografia en què els serveis no es criden entre ells, sinó que publiquen i consumeixen esdeveniments. Fins ara aquests esdeveniments (comanda.creada, estoc.reservat, pagament.confirmat, comanda.confirmada, comanda.cancellada) han estat noms en un diagrama. Aquesta lliçó els converteix en missatges reals que viatgen per RabbitMQ.

Veurem primer quan convé la comunicació asíncrona i quin acoblament elimina; després el vocabulari imprescindible (missatge, esdeveniment i comanda-ordre, cua i pub-sub, broker, ack, relliurament, dead-letter queue, ordre i garanties de lliurament); a continuació RabbitMQ en detall (exchanges, routing keys, cues, bindings) amb la topologia concreta de TechCorp; el codi amb amqplib per publicar el sobre d'esdeveniment estàndard i per consumir amb prefetch, ack/nack i reenviament a la DLQ; l'enllaç amb l'outbox i la idempotència de 02-05; i una comparació de RabbitMQ amb Kafka i les cues gestionades del núvol que justifica l'elecció de TechCorp. La integració de punta a punta d'un servei real (amb el seu outbox, els seus consumidors arrencant al costat d'Express) és de 04-04, i desplegar el broker és del mòdul 5.

Contingut

  1. Síncron davant d'asíncron: quin acoblament eliminem
  2. Vocabulari de la missatgeria
  3. Garanties de lliurament: at-most-once, at-least-once, exactly-once
  4. RabbitMQ: exchanges, cues, routing keys i bindings
  5. La topologia de TechCorp
  6. Publicar un esdeveniment amb amqplib
  7. Consumir esdeveniments: prefetch, ack, nack i DLQ
  8. Enllaç amb outbox i idempotència
  9. RabbitMQ davant de Kafka i cues gestionades

  1. Síncron davant d'asíncron: quin acoblament eliminem

Quan Comandes crida GET /productes?ids= i n'espera la resposta, Comandes i Catàleg han d'estar vius en el mateix instant: és l'acoblament temporal de 02-01. Amb sis serveis encadenats al flux de comanda, aquest acoblament seria fatal: la disponibilitat del conjunt seria el producte de les sis, i una caiguda de Notificacions impediria crear comandes. La missatgeria asíncrona trenca aquesta cadena: Comandes deixa l'esdeveniment al broker i continua; Inventari el recollirà quan pugui, encara que sigui al cap d'un minut.

Criteri Síncron (REST, gRPC) Asíncron (missatges)
Qui crida espera la resposta No; continua la seva feina
Acoblament temporal Sí: tots dos han d'estar disponibles No: el broker guarda el missatge
Qui coneix qui Qui crida coneix el receptor (URL) El productor no sap qui consumeix
Fallada del receptor Error immediat a qui crida El missatge espera; es processa quan torna
Pics de càrrega El receptor els pateix en temps real La cua els amorteix
Model mental Pregunta-resposta Fet ocorregut / ordre donada
Depuració Senzilla: una traça lineal Més difícil: causa i efecte separats en el temps (06-02)
Quan la fa servir TechCorp Quan necessitem la resposta per continuar: consultar preu i nom abans de desar la comanda Quan el resultat no fa falta ara: reservar, cobrar, notificar

La regla pràctica que aplicarà TechCorp: per defecte asíncron entre serveis; síncron només quan la resposta és imprescindible per respondre a l'usuari. És la mateixa conclusió de 02-02, ara amb la justificació tècnica.

  1. Vocabulari de la missatgeria

  • Missatge. Unitat de dades que viatja pel broker: unes capçaleres i un cos (a TechCorp, el sobre JSON de 02-05).
  • Esdeveniment davant d'ordre (command). Un esdeveniment descriu una cosa que ja ha passat (comanda.creada); qui el publica no espera res de ningú, i hi pot haver zero o molts consumidors. Una ordre (command) és un manament dirigit a un receptor concret (reservar-estoc): qui l'envia espera que algú l'executi. La saga per coreografia de 02-05 fa servir només esdeveniments; una saga per orquestració faria servir ordres. La distinció importa perquè canvia qui decideix: amb esdeveniments, decideix el consumidor si li interessa; amb ordres, decideix l'emissor a qui les envia.
  • Productor (publisher) i consumidor (subscriber). Qui envia i qui rep. Un servei sol ser totes dues coses: Inventari consumeix comanda.creada i produeix estoc.reservat.
  • Broker. L'intermediari que rep, emmagatzema i lliura missatges: RabbitMQ, Kafka, SQS. És la "canonada ximple" de 02-01: encamina, no decideix.
  • Cua (queue). Un buffer FIFO del qual un o diversos consumidors competeixen pels missatges: cada missatge el processa una instància. Ideal per repartir feina entre rèpliques del mateix servei.
  • Tòpic / pub-sub. Un missatge es copia a tots els subscriptors interessats. Ideal per a esdeveniments: comanda.confirmada interessa a Notificacions i a Inventari, i tots dos l'han de rebre. A RabbitMQ, la combinació "un exchange que reparteix a diverses cues, cada cua amb les rèpliques d'un servei competint" dona els dos comportaments alhora.
  • Ack / nack. El consumidor confirma (acknowledge) que ha processat el missatge; fins llavors el broker el considera pendent. Si el consumidor el rebutja (negative ack) o mor sense confirmar, el broker el torna a lliurar (redelivery), a la mateixa instància o a una altra.
  • Dead-letter queue (DLQ). Cua on va a parar un missatge que no s'ha pogut processar (rebutjat sense reencuar, caducat o expulsat per límit). Evita el "missatge verinós" que es relliura infinitament i permet inspeccionar-lo i reprocessar-lo a mà.
  • Ordre. RabbitMQ conserva l'ordre dins d'una cua per a un sol consumidor; amb diverses rèpliques consumint en paral·lel, l'ordre global no està garantit. Per això el disseny de 02-05 fa que cada esdeveniment sigui autocontingut i que la màquina d'estats de la comanda rebutgi transicions fora d'ordre.

  1. Garanties de lliurament: at-most-once, at-least-once, exactly-once

Garantia Com s'aconsegueix Què pot passar Quan serveix
At-most-once (com a molt una vegada) El broker lliura i oblida; el consumidor fa ack abans de processar (o no hi ha ack) Es perden missatges si el consumidor falla a mitges Mètriques, logs no crítics
At-least-once (almenys una vegada) Missatges persistents; el consumidor fa ack després de processar; si falla, relliurament Es dupliquen missatges: si el consumidor processa i mor abans de l'ack, el rep un altre cop Tot el de negoci: comandes, pagaments
Exactly-once (exactament una vegada) Requereix que broker i consumidor comparteixin una transacció, o que el consumidor dedupliqui A la pràctica no existeix d'extrem a extrem entre sistemes diferents (el broker no pot saber si el teu UPDATE a PostgreSQL s'ha confirmat) Només dins d'un mateix sistema (Kafka Streams entre tòpics de Kafka)

La conclusió realista, que ja va avançar 02-05: triem at-least-once + idempotència. El broker garanteix que cap esdeveniment no es perd (encara que algun arribi dues vegades), i el consumidor garanteix que processar dues vegades el mateix esdevenimentId no té cap efecte (processarUnCop() amb la taula esdeveniments_processats). És l'única combinació que funciona amb una base de dades per servei.

  1. RabbitMQ: exchanges, cues, routing keys i bindings

RabbitMQ implementa el protocol AMQP 0-9-1, el model del qual té quatre peces:

  • Exchange. El productor mai no publica directament en una cua: publica en un exchange amb una routing key (una cadena com comanda.creada). L'exchange decideix a quines cues copiar el missatge segons el seu tipus:
    • direct: a les cues el binding de les quals coincideixi exactament amb la routing key.
    • fanout: a totes les cues enllaçades, ignorant la routing key.
    • topic: a les cues el patró de binding de les quals encaixi amb la routing key, amb comodins: * substitueix exactament una paraula (separades per punts) i # zero o més. comanda.* encaixa amb comanda.creada i comanda.cancellada; # encaixa amb tot.
    • headers: encamina per capçaleres; poc usat.
  • Cua. On esperen els missatges. Pot ser duradora (sobreviu a un reinici del broker) i contenir missatges persistents (escrits a disc). Per a at-least-once calen totes dues coses.
  • Binding. La regla que uneix un exchange amb una cua: "els missatges amb routing key que encaixi amb comanda.creada van a la cua inventari.comandes".
  • Canal (channel). Connexió lògica multiplexada dins d'una connexió TCP; cada fil o consumidor fa servir el seu.

Un missatge arriba a un exchange amb la seva routing key, l'exchange el copia a cada cua el binding de la qual encaixa (així s'aconsegueix el pub-sub), i dins de cada cua les rèpliques del servei consumidor competeixen (així s'aconsegueix el repartiment de càrrega). Un mateix missatge pot acabar en tres cues i ser processat un cop en cadascuna.

  1. La topologia de TechCorp

Decisions:

  • Un únic exchange de tipus topic, anomenat techcorp.esdeveniments, durador. Tots els serveis hi publiquen.
  • La routing key és igual al tipus d'esdeveniment: comanda.creada, estoc.reservat, pagament.confirmat, etc. Així el nom de l'esdeveniment i el seu encaminament són la mateixa cosa i no cal mantenir dos vocabularis.
  • Una cua per servei consumidor, amb nom <consumidor>.<tema>, amb bindings als esdeveniments que li interessen. Les rèpliques d'un servei comparteixen la seva cua (repartiment de càrrega); serveis diferents tenen cues diferents (cadascun rep la seva còpia).
  • Cada cua té una DLQ associada (<cua>.dlq) a través d'un exchange techcorp.esdeveniments.dlx de tipus direct.
Cua Servei Bindings (routing keys) Què en fa
inventari.comandes Inventari comanda.creada, comanda.confirmada, comanda.cancellada Reserva estoc; consumeix la reserva; allibera la reserva
pagaments.estoc Pagaments estoc.reservat, comanda.cancellada Cobra quan hi ha reserva; reemborsa si la comanda es cancel·la després de cobrar
notificacions.comandes Notificacions comanda.confirmada, comanda.cancellada Envia correu de confirmació o de cancel·lació
comandes.saga Comandes estoc.reservat, estoc.rebutjat, pagament.confirmat, pagament.rebutjat Fa avançar la màquina d'estats de la comanda i publica comanda.confirmada / comanda.cancellada
comandes.clients Comandes client.actualitzat Manté la rèplica clients_ref de 02-04
flowchart LR
    subgraph Productors
        P[servei-comandes]
        I[servei-inventari]
        G[servei-pagaments]
        C[servei-clients]
    end
    X{{"exchange techcorp.esdeveniments (topic)"}}
    P -- "comanda.creada / comanda.confirmada / comanda.cancellada" --> X
    I -- "estoc.reservat / estoc.rebutjat / estoc.alliberat" --> X
    G -- "pagament.confirmat / pagament.rebutjat / pagament.reemborsat" --> X
    C -- "client.actualitzat" --> X
    X -- "comanda.creada, comanda.confirmada, comanda.cancellada" --> Q1[(inventari.comandes)]
    X -- "estoc.reservat, comanda.cancellada" --> Q2[(pagaments.estoc)]
    X -- "comanda.confirmada, comanda.cancellada" --> Q3[(notificacions.comandes)]
    X -- "estoc.*, pagament.*" --> Q4[(comandes.saga)]
    X -- "client.actualitzat" --> Q5[(comandes.clients)]
    Q1 --> CI[Inventari x N rèpliques]
    Q2 --> CG[Pagaments x N]
    Q3 --> CN[Notificacions x N]
    Q4 --> CP[Comandes x N]
    Q5 --> CP
    Q1 -. "nack sense requeue" .-> DLX{{"techcorp.esdeveniments.dlx"}}
    Q2 -.-> DLX
    Q3 -.-> DLX
    Q4 -.-> DLX
    DLX --> D1[(inventari.comandes.dlq)]
    DLX --> D2[(pagaments.estoc.dlq)]
    DLX --> D3[(notificacions.comandes.dlq)]
    DLX --> D4[(comandes.saga.dlq)]

Fixa't que Comandes publica comanda.confirmada en rebre pagament.confirmat, i aquest esdeveniment el consumeixen dues cues diferents (Notificacions i Inventari): la còpia la fa l'exchange, no Comandes. Comandes no sap ni li importa quants consumidors hi ha: això és la "conformitat" de Notificacions i l'asimetria del mapa de contextos de 02-03 fetes topologia.

Sobre comandes.saga amb el binding estoc.* i pagament.*: és còmode, però també rebria estoc.alliberat i pagament.reemborsat, que Comandes no necessita. A la pràctica es declaren els quatre bindings explícits de la taula; el comodí apareix al diagrama només per brevetat.

  1. Publicar un esdeveniment amb amqplib

amqplib és la llibreria estàndard de Node.js per a AMQP 0-9-1. Primer, la declaració de la topologia. Cada servei declara en arrencar allò que fa servir (l'exchange i les seves pròpies cues); assert* és idempotent: si ja existeix amb els mateixos paràmetres, no fa res.

// missatgeria/topologia.js (compartit via @techcorp/comu-http o copiat a cada servei)
const amqp = require('amqplib');

const EXCHANGE = 'techcorp.esdeveniments';
const EXCHANGE_DLX = 'techcorp.esdeveniments.dlx';

async function connectar(url = process.env.RABBITMQ_URL ?? 'amqp://localhost:5672') {
  // 1. Una connexió TCP per procés...
  const connexio = await amqp.connect(url);
  // 2. ...i un canal per ús (aquí un per publicar; els consumidors obriran el seu)
  const canal = await connexio.createChannel();
  // 3. Declarar l'exchange principal (topic, durador) i el de dead-letter (direct, durador)
  await canal.assertExchange(EXCHANGE, 'topic', { durable: true });
  await canal.assertExchange(EXCHANGE_DLX, 'direct', { durable: true });
  return { connexio, canal };
}

// Declara una cua de consumidor amb la seva DLQ i els seus bindings
async function declararCuaConsumidor(canal, nomCua, routingKeys) {
  // 4. La DLQ: cua duradora enllaçada a l'exchange DLX amb la routing key = nom de la cua original
  await canal.assertQueue(`${nomCua}.dlq`, { durable: true });
  await canal.bindQueue(`${nomCua}.dlq`, EXCHANGE_DLX, nomCua);
  // 5. La cua principal: duradora i amb dead-lettering configurat
  await canal.assertQueue(nomCua, {
    durable: true,
    arguments: {
      'x-dead-letter-exchange': EXCHANGE_DLX,     // on van els missatges rebutjats
      'x-dead-letter-routing-key': nomCua         // amb quina routing key (→ la seva .dlq)
    }
  });
  // 6. Un binding per cada tipus d'esdeveniment que interessa a aquest consumidor
  for (const rk of routingKeys) {
    await canal.bindQueue(nomCua, EXCHANGE, rk);
  }
}

module.exports = { connectar, declararCuaConsumidor, EXCHANGE };

Ara la publicació. La funció rep el sobre d'esdeveniment estàndard de 02-05 ja construït (esdevenimentId, tipus, versio, ocorregutEn, carrega) i l'envia:

// missatgeria/publicador.js
const { randomUUID } = require('node:crypto');
const { EXCHANGE } = require('./topologia');

function construirSobre(tipus, carrega, { versio = 1 } = {}) {
  return {
    esdevenimentId: `evt-${randomUUID()}`, // id únic: el fa servir processarUnCop() del consumidor
    tipus,                                  // 'comanda.creada'
    versio,                                 // versió de l'esquema de la càrrega (03-06)
    ocorregutEn: new Date().toISOString(),  // quan ha passat, en UTC
    carrega                                 // el JSON de negoci
  };
}

function publicarEsdeveniment(canal, sobre) {
  const cos = Buffer.from(JSON.stringify(sobre));
  // publish retorna false si el buffer intern és ple (backpressure); ho tractem a 06-04
  return canal.publish(
    EXCHANGE,        // exchange destí
    sobre.tipus,     // routing key = tipus d'esdeveniment
    cos,
    {
      persistent: true,                     // s'escriu a disc: sobreviu a un reinici del broker
      contentType: 'application/json',
      messageId: sobre.esdevenimentId,      // dupliquem l'id a la capçalera AMQP per a les eines
      type: sobre.tipus,
      timestamp: Math.floor(Date.now() / 1000),
      headers: { 'x-version': sobre.versio }
    }
  );
}

module.exports = { construirSobre, publicarEsdeveniment };

I el seu ús des del relay de l'outbox de Comandes, amb l'esdeveniment comanda.creada de la comanda com-88213 (el JSON de càrrega és el que vam fixar a 02-05):

const sobre = construirSobre('comanda.creada', {
  comandaId: 'com-88213',
  clientId: 'c-1024',
  client: { email: '[email protected]', nom: 'Ana Ruiz' },
  adrecaEnviament: { carrer: 'Gran Vía 12', codiPostal: '28013', ciutat: 'Madrid', pais: 'ES' },
  linies: [
    { producteId: 'p-501', nom: 'Auriculars BT X200', quantitat: 1, preuUnitari: 59.90 },
    { producteId: 'p-777', nom: 'Cable USB-C 2 m', quantitat: 2, preuUnitari: 9.90 }
  ],
  total: 79.70
});
publicarEsdeveniment(canal, sobre);

Tres detalls que marquen la diferència entre "funciona a la meva màquina" i "no perd comandes":

  1. persistent: true i cues durable: true van plegats. Un missatge persistent en una cua no duradora es perd igualment en reiniciar; una cua duradora amb missatges no persistents, també.
  2. Confirmacions del productor. Amb createChannel(), publish és "dispara i oblida": si RabbitMQ cau just llavors, el missatge es perd sense error. Amb createConfirmChannel() el broker confirma cada publicació i podem esperar amb await canal.waitForConfirms() abans de marcar la fila de l'outbox com a enviada. És el que farà servir el relay a 04-04.
  3. L'esdevenimentId viatja al cos i a messageId. Al cos perquè forma part del contracte de l'esdeveniment; a la capçalera perquè la consola de RabbitMQ i les eines de DLQ el mostren sense obrir el JSON.

  1. Consumir esdeveniments: prefetch, ack, nack i DLQ

El consumidor d'Inventari per a la cua inventari.comandes:

// missatgeria/consumidorInventari.js (servei-inventari)
const { connectar, declararCuaConsumidor } = require('./topologia');

async function iniciarConsumidor({ gestors, processarUnCop }) {
  const { connexio, canal } = await connectar();
  const CUA = 'inventari.comandes';

  await declararCuaConsumidor(canal, CUA, ['comanda.creada', 'comanda.confirmada', 'comanda.cancellada']);

  // 1. prefetch: quants missatges sense ack pot tenir aquesta instància alhora.
  //    Sense això, RabbitMQ bolcaria tota la cua a la primera rèplica que es connectés.
  await canal.prefetch(10);

  await canal.consume(CUA, async (msg) => {
    if (msg === null) return; // el canal s'ha tancat
    let sobre;
    try {
      sobre = JSON.parse(msg.content.toString());
    } catch (err) {
      // 2. Missatge que ni tan sols és JSON: no té sentit reintentar → a la DLQ
      //    nack(msg, allUpTo=false, requeue=false) → RabbitMQ l'envia al DLX configurat
      console.error('Missatge il·legible, enviat a la DLQ', { messageId: msg.properties.messageId });
      return canal.nack(msg, false, false);
    }

    const gestor = gestors[sobre.tipus];
    if (!gestor) {
      // 3. Esdeveniment amb binding però sense gestor (p. ex. una versió desplegada a mitges): DLQ, no perdre'l
      console.warn('Sense gestor per al tipus', { tipus: sobre.tipus, esdevenimentId: sobre.esdevenimentId });
      return canal.nack(msg, false, false);
    }

    try {
      // 4. processarUnCop (02-05): si esdevenimentId ja és a esdeveniments_processats per a 'inventari', no fa res
      await processarUnCop(sobre.esdevenimentId, 'inventari', () => gestor(sobre));
      // 5. Tot bé: ack. Només ara RabbitMQ esborra el missatge de la cua
      canal.ack(msg);
    } catch (err) {
      // 6. Error en processar. És transitori (BD caiguda) o permanent (dades impossibles)?
      const esPrimerCop = !msg.fields.redelivered;
      if (err.transitori && esPrimerCop) {
        // Reencuar UNA vegada: RabbitMQ el tornarà a lliurar (a aquesta rèplica o a una altra)
        console.warn('Error transitori, reencuant', { esdevenimentId: sobre.esdevenimentId, error: err.message });
        canal.nack(msg, false, true);
      } else {
        // Segona fallada o error permanent: a la DLQ per a inspecció humana
        console.error('Esdeveniment enviat a la DLQ', { esdevenimentId: sobre.esdevenimentId, tipus: sobre.tipus, error: err.message });
        canal.nack(msg, false, false);
      }
    }
  }, { noAck: false }); // 7. noAck: false = mode at-least-once (ack manual). És el valor per defecte, però s'explicita

  // 8. Tancament ordenat: en aturar el procés, tancar canal i connexió perquè els missatges sense ack es relliurin de seguida
  process.on('SIGTERM', async () => { await canal.close(); await connexio.close(); });
}

module.exports = { iniciarConsumidor };

I els gestors que Inventari registra (només la signatura; la lògica de reserva és del mòdul 4):

iniciarConsumidor({
  processarUnCop,
  gestors: {
    'comanda.creada':     (sobre) => reservarEstocPerAComanda(sobre.carrega),   // → publica estoc.reservat o estoc.rebutjat
    'comanda.confirmada': (sobre) => consumirReserva(sobre.carrega.comandaId),   // ACTIVA → CONSUMIDA
    'comanda.cancellada': (sobre) => alliberarReserva(sobre.carrega.comandaId)   // ACTIVA → ALLIBERADA, publica estoc.alliberat
  }
});

Punts que convé entendre bé:

  • prefetch(10) limita la feina en vol per rèplica. Un valor baix reparteix millor entre rèpliques i evita que un procés que mor arrossegui centenars de missatges cap al relliurament; un valor alt augmenta el rendiment. Deu és un punt de partida raonable per a gestors que toquen la base de dades.
  • ack després de processar és el que dona at-least-once. Si el procés mor entre gestor() i ack, RabbitMQ relliura i processarUnCop evita l'efecte doble. No facis mai ack al principi "perquè no s'encalli": això és at-most-once amb un nom bonic.
  • nack(msg, false, requeue): el segon argument (allUpTo) rebutja també tots els anteriors sense ack; gairebé sempre false. El tercer decideix entre reencuar (true) i descartar/dead-letter (false).
  • Reencuar sense límit és un bucle infinit. Un missatge que falla sempre tornaria al cap de la cua i bloquejaria els altres. Per això es reencua com a molt un cop (msg.fields.redelivered diu si ja ho ha estat) i després va a la DLQ. Els reintents amb espera creixent es veuen a 06-03; RabbitMQ no els dona de sèrie.
  • La DLQ no es consumeix automàticament. Algú (una alerta de 06-05 i una persona) mira inventari.comandes.dlq, entén per què ha fallat i decideix si reprocessar (moure el missatge de tornada) o descartar.

  1. Enllaç amb outbox i idempotència

Amb el que hem vist, la cadena completa de garanties per a "un client fa una comanda" queda així, i val la pena veure-la tota junta encara que cada peça es dissenyés a 02-05:

Risc Peça que el cobreix On viu
Es desa la comanda però no es publica comanda.creada (o a l'inrevés) Outbox transaccional: comanda i esdeveniment a la mateixa transacció; un relay llegeix outbox i crida publicarEsdeveniment Comandes (desarAmbEsdeveniments())
El relay publica i RabbitMQ cau abans de desar-lo persistent: true + cua duradora + canal de confirmacions publicador.js
Inventari processa i mor abans de l'ack → relliurament Idempotència del consumidor: processarUnCop(esdevenimentId, 'inventari', fn) amb esdeveniments_processats Cada consumidor
El relay reenvia la mateixa fila d'outbox dues vegades Mateix esdevenimentId a totes dues còpies → el consumidor la deduplica Contracte del sobre
Un missatge impossible de processar bloqueja la cua DLQ Topologia
L'usuari reenvia POST /comandes Idempotency-Key (03-01) Comandes

Res d'això no és exòtic: és at-least-once a cada salt més una deduplicació per esdevenimentId a la destinació. És el preu de no tenir transaccions distribuïdes, i és barat.

  1. RabbitMQ davant de Kafka i cues gestionades

Criteri RabbitMQ Apache Kafka Cues gestionades (AWS SQS/SNS, Google Pub/Sub)
Model Broker AMQP: exchanges, cues, encaminament flexible; el missatge s'esborra en fer ack Log distribuït: tòpics particionats, els missatges es retenen (dies o per sempre); els consumidors guarden el seu offset Cua (SQS) + pub-sub (SNS) o tòpic amb subscripcions (Pub/Sub); sense operar res
Encaminament Molt ric (topic amb comodins, headers) Per tòpic i partició; el filtratge el fa el consumidor Filtres de subscripció senzills
Ordre Per cua amb un consumidor Per partició, garantit; molt fort Només amb cues FIFO / ordering keys
Reproduir missatges antics No (un cop consumit, s'ha esfumat) : rellegir des d'un offset; base de l'event sourcing No (o retenció curta)
Rendiment Desenes de milers de msg/s per node Milions de msg/s; dissenyat per a streaming Elàstic; pagues per ús
Operació Un clúster senzill; consola web excel·lent Més complex (particions, ZooKeeper/KRaft, rebalancejos) Nul·la, però lock-in amb el proveïdor
Latència Molt baixa (ms) Baixa, però orientada a lots Variable (desenes de ms)
Corba d'aprenentatge Suau; conceptes intuïtius Pronunciada; nou model mental Suau
Encaixa quan Esdeveniments de negoci i ordres entre serveis, encaminament variat, volum mitjà Streaming, analítica, reprocessament històric, volums enormes Ja ets en aquell núvol i no vols operar un broker

Per què TechCorp tria RabbitMQ:

  • El volum (~3.000 comandes/dia, uns quants esdeveniments per comanda) és a ordres de magnitud del llindar en què Kafka compensa la seva complexitat operativa. Amb ~25 tècnics, l'equip de Plataforma no pot dedicar ningú a tenir cura de particions.
  • L'encaminament per topic amb una cua per consumidor modela de manera directa el mapa de contextos de 02-03: cada servei se subscriu al que li interessa i ningú més no se n'assabenta.
  • La DLQ, el prefetch i les confirmacions cobreixen les garanties que la saga necessita sense codi extra.
  • A 02-05 vam descartar l'event sourcing "de moment"; si algun dia s'adopta, la retenció i el reprocessament de Kafka serien el motiu per migrar, i el sobre d'esdeveniment estàndard farà aquesta migració menys dolorosa.
  • La Marta va descartar les cues gestionades per no lligar el sistema a un núvol en un moment en què encara es decideix on correrà Kubernetes.

Queda un tema que aquesta lliçó ha fregat a cada fragment: la forma de la càrrega de cada esdeveniment (quins camps porta comanda.creada, què passa quan cal afegir-ne un o canviar-ne un altre) i aquell camp versio del sobre. És el contracte dels esdeveniments, i es tracta juntament amb el de les APIs a 03-06.

Errors Comuns i Consells

  • Publicar directament en una cua (sendToQueue) en lloc de fer-ho a l'exchange. Funciona fins que un segon servei necessita el mateix esdeveniment; llavors cal tocar el productor. Amb l'exchange només s'afegeix un binding.
  • ack abans de processar "perquè vagi ràpid". Converteix at-least-once en at-most-once: un reinici a mitges i la comanda es queda sense reserva per sempre.
  • Reencuar sense límit (nack(msg, false, true) a tots els catch). Un missatge verinós monopolitza la cua. Un relliurament i cap a la DLQ.
  • Oblidar prefetch. La primera rèplica que arrenca s'emporta tots els missatges pendents; les altres es queden mirant.
  • Cua no duradora o missatge no persistent. Tot es veu bé fins al primer reinici del broker. Tots dos flags, sempre, per a esdeveniments de negoci.
  • Càrregues d'esdeveniment que apunten a dades ({"comandaId": "com-88213"} i que el consumidor cridi GET /comandes/com-88213). Reintrodueix l'acoblament temporal que volíem treure. L'esdeveniment porta el que el consumidor necessita (per això comanda.creada inclou línies, preus i correu).
  • Confiar en l'ordre entre cues o entre rèpliques. Dissenya els consumidors perquè tolerin pagament.confirmat abans que la seva pròpia BD reflecteixi estoc.reservat (la màquina d'estats de 02-05 i processarUnCop hi ajuden).
  • Ignorar la DLQ. Sense alerta sobre *.dlq, els missatges s'acumulen durant mesos. A 06-05 es defineix l'alerta; des d'avui, mira-la a la consola de RabbitMQ.
  • Una connexió per petició. Les connexions AMQP són cares. Una connexió per procés, un canal per consumidor o publicador, reutilitzats.

Exercicis

Exercici 1. L'equip de Pagaments i comunicacions vol que Notificacions enviï també un correu "hem rebut la teva comanda" tan bon punt es crea (a més del de confirmació). Indica quin binding cal afegir, a quina cua, i què no cal tocar. Després, raona: tindria sentit que Notificacions fes servir la mateixa cua notificacions.comandes o una de nova notificacions.comandes-rebudes? Dona un argument a favor de cada opció.

Exercici 2. Un consumidor de Pagaments processa estoc.reservat, crida la passarel·la externa, que cobra 79,70 € a l'Ana, i just abans de l'ack la instància mor. RabbitMQ relliura el missatge a una altra rèplica. Explica pas a pas què passa amb i sense processarUnCop, i quina garantia addicional necessita el consumidor de Pagaments respecte de la passarel·la (pista: 03-01 en va parlar amb un altre nom).

Exercici 3. Escriu una funció moureDeDlqACua(canal, nomCua, maxim) amb amqplib que llegeixi fins a maxim missatges de ${nomCua}.dlq amb canal.get() (obtenció síncrona, sense subscripció) i els torni a publicar a l'exchange techcorp.esdeveniments amb la routing key original (disponible a msg.fields.routingKey o, després del dead-lettering, a la capçalera x-death), conservant persistent: true, i faci ack de cadascun a la DLQ només després de republicar-lo. Comenta cada línia.

Solucions

Solució 1.

N'hi ha prou d'afegir el binding comanda.creada a la cua de Notificacions (bindQueue('notificacions.comandes', 'techcorp.esdeveniments', 'comanda.creada')) i registrar un gestor 'comanda.creada' al seu consumidor. No cal tocar Comandes (continua publicant exactament igual), ni Inventari, ni l'exchange: això és el que compra el pub-sub per topic. La càrrega de comanda.creada ja porta client.email i client.nom precisament perquè Notificacions no hagi de cridar ningú.

Mateixa cua: més simple, un sol consumidor, un sol prefetch, una sola DLQ a vigilar; l'ordre relatiu "rebuda → confirmada" per a una mateixa comanda es conserva millor. Cua nova: aïlla la fallada (si el correu de "rebuda" es trenca i omple la DLQ, els de confirmació continuen sortint) i permet escalar i prioritzar per separat (els de confirmació són més importants). Per a TechCorp, amb el volum actual, la mateixa cua; separar-les quan hi hagi un motiu operatiu.

Solució 2.

Sense processarUnCop: la segona rèplica rep el mateix estoc.reservat (mateix esdevenimentId), no sap que ja s'ha processat, torna a cridar la passarel·la i cobra 79,70 € dues vegades; a més publica dos pagament.confirmat. Amb processarUnCop: la segona rèplica consulta esdeveniments_processats per a (evt-..., 'pagaments')... i aquí hi ha la trampa: si la primera rèplica va morir abans de confirmar la transacció que insereix a esdeveniments_processats, l'esdeveniment no consta com a processat, i la segona rèplica també cobrarà. processarUnCop protegeix contra "he processat i he mort abans de l'ack" només si l'efecte i el registre a esdeveniments_processats són a la mateixa transacció local, i la crida a la passarel·la externa no pot ser dins d'aquella transacció.

La garantia addicional: la passarel·la ha d'acceptar una clau d'idempotència (la majoria de passarel·les reals ho fan), i Pagaments ha de fer servir com a clau alguna cosa derivada de la comanda (com-88213) o de l'esdevenimentId, exactament el mateix concepte que la capçalera Idempotency-Key de 03-01, però de Pagaments cap enfora. Així, la segona crida a la passarel·la retorna el mateix càrrec en lloc de crear-ne un altre. Regla general: la idempotència és necessària a cada frontera on hi ha un efecte no reversible.

Solució 3.

async function moureDeDlqACua(canal, nomCua, maxim = 100) {
  const dlq = `${nomCua}.dlq`;
  let moguts = 0;
  for (let i = 0; i < maxim; i++) {
    // get() obté UN missatge sense subscriure-s'hi; retorna false si la DLQ és buida
    const msg = await canal.get(dlq, { noAck: false });
    if (!msg) break;

    // Després del dead-lettering, msg.fields.routingKey és la del DLX (= nomCua).
    // La routing key original (p. ex. 'comanda.creada') queda registrada a la capçalera x-death.
    const xDeath = msg.properties.headers?.['x-death']?.[0];
    const routingKeyOriginal = xDeath?.['routing-keys']?.[0] ?? msg.properties.type;
    if (!routingKeyOriginal) {
      // Sense manera de saber on anava: el deixem a la DLQ (nack amb requeue) i continuem
      canal.nack(msg, false, true);
      continue;
    }

    // Republiquem a l'exchange principal amb les mateixes propietats (persistent, messageId, type...)
    canal.publish('techcorp.esdeveniments', routingKeyOriginal, msg.content, {
      ...msg.properties,
      persistent: true,
      headers: { ...msg.properties.headers, 'x-reprocessat-des-de-dlq': dlq }
    });
    // Només després de republicar fem ack a la DLQ: si el procés mor entremig,
    // el missatge continua a la DLQ (podria duplicar-se, però processarUnCop ho absorbeix)
    canal.ack(msg);
    moguts++;
  }
  return moguts;
}

Notes: msg.properties.type l'omplim a publicarEsdeveniment amb sobre.tipus, de manera que serveix de reserva si x-death no hi fos. Amb un canal de confirmacions seria encara més segur esperar waitForConfirms() abans de l'ack. I sí, aquest script pot duplicar un esdeveniment en el pitjor dels casos; com tot en aquesta lliçó, la idempotència del consumidor és el que ho fa inofensiu.

Conclusió

La missatgeria asíncrona és el que fa possible la saga de 02-05: elimina l'acoblament temporal entre serveis, amorteix pics i permet que un mateix fet (comanda.confirmada) arribi a diversos interessats sense que el productor en sàpiga res. Hem fixat el vocabulari (esdeveniment davant d'ordre, cua davant de pub-sub, ack/nack, relliurament, DLQ), hem acceptat at-least-once + idempotència com la garantia realista, i hem construït la topologia de TechCorp a RabbitMQ: un exchange topic techcorp.esdeveniments, routing keys iguals al tipus d'esdeveniment, una cua duradora per consumidor (inventari.comandes, pagaments.estoc, notificacions.comandes, comandes.saga, comandes.clients) amb la seva DLQ, i el codi amqplib per publicar el sobre estàndard amb persistent: true i per consumir amb prefetch, ack després de processar i nack cap a la DLQ. Amb REST per al que és síncron i RabbitMQ per al que és asíncron, TechCorp ja té els seus dos canals principals.

Però REST/JSON no és l'única forma de crida síncrona ni sempre la millor: quan dos serveis interns es parlen milers de vegades per minut, la verbositat del JSON i la manca de contracte tipat pesen, i quan un front-end necessita compondre dades de diversos serveis, REST obliga a moltes peticions o a respostes enormes. Per al primer cas hi ha gRPC; per al segon, GraphQL. A la lliçó següent veurem tots dos, amb el seu codi en Node.js, i decidirem on encaixen (i on no) a TechCorp.

Curs de Microserveis

Mòdul 1: Introducció als Microserveis

Mòdul 2: Disseny de Microserveis

Mòdul 3: Comunicació entre Microserveis

Mòdul 4: Implementació de Microserveis

Mòdul 5: Desplegament i Orquestració

Mòdul 6: Monitoratge i Manteniment

Mòdul 7: Seguretat en Microserveis

Mòdul 8: Casos d'Estudi i Exemples Pràctics

© Copyright 2026. Tots els drets reservats