A la lliçó anterior vam partir PideYa en serveis, però vam deixar pendent la pregunta de fons: com parlen entre si, i què passa quan la conversa falla? Un sistema distribuït és aquell en què la fallada d'una màquina que ni coneixies pot deixar el teu programa inservible (la cèlebre definició de Leslie Lamport). Aquesta lliçó cobreix els patrons de comunicació i de dades entre nodes: del proxy remot que vam prometre al mòdul 3 a la missatgeria asíncrona, les garanties de lliurament, el patró Outbox i la consistència eventual. El fil GoF continua: aquí Proxy i Observer s'estiren sobre la xarxa fins a gairebé no reconèixer-se — però la intenció és la mateixa.

Contingut

  1. Proxy remot i RPC: la crida que creua la xarxa
  2. Les 8 fal·làcies de la computació distribuïda
  3. Missatgeria asíncrona: cues productor-consumidor
  4. Pub-sub i brokers: Observer a escala de xarxa
  5. Garanties de lliurament i idempotència
  6. El patró Outbox
  7. Arquitectura orientada a esdeveniments a PideYa
  8. Consistència eventual i el teorema CAP
  9. Resiliència complementària: timeout, bulkhead i dead letter queue

Proxy remot i RPC

A 03-08 vam deixar una porta oberta: el proxy remot, promès per a aquesta lliçó. La idea de l'RPC (Remote Procedure Call) és que cridar un altre procés sembli cridar un mètode local:

// El client del servei de Pagaments usa la MATEIXA interficie que el domini
PassarellaPagament pagaments = clientRpc.crearProxy(PassarellaPagament.class, "http://pagos:8080");
ResultatCobrament r = pagaments.cobrar(comanda, targeta); // sembla local... pero viatja per xarxa

Aquest crearProxy genera un Proxy GoF (amb els proxies dinàmics que vam veure a 03-08) que serialitza els arguments, els envia per HTTP o gRPC, espera la resposta i la deserialitza. Els stubs de gRPC, els clients Feign de Spring o els vells RMI són exactament això: el patró Proxy amb la xarxa a dins.

La trampa és que la transparència és mentida. Una crida local falla d'una manera (excepció); una de remota falla de tres: abans d'arribar, en executar-se, o en tornar la resposta — i en el tercer cas l'efecte sí que va ocórrer encara que tu vegis un timeout. Aquesta "fallada parcial" no existeix dins d'un procés i és la raó de gairebé tots els patrons d'aquesta lliçó.

Les 8 fal·làcies de la computació distribuïda

Formulades a Sun Microsystems (Deutsch, Gosling i altres), són les suposicions falses que tothom fa al principi:

# Fal·làcia Conseqüència real a PideYa
1 La xarxa és fiable Els missatges es perden: calen reintents i confirmacions
2 La latència és zero 200 crides al catàleg per pintar la carta = pantalla lentíssima
3 L'ample de banda és infinit Enviar la comanda sencera en cada esdeveniment satura el broker
4 La xarxa és segura Els serveis s'han d'autenticar entre si (mTLS, tokens)
5 La topologia no canvia Les IPs canvien amb cada desplegament: d'aquí el Service Discovery de 06-02
6 Hi ha un sol administrador El proveïdor cloud reinicia nodes sense avisar-te
7 El cost de transport és zero Serialitzar/deserialitzar consumeix CPU real
8 La xarxa és homogènia El datacenter i el mòbil del repartidor dins d'un túnel no s'assemblen

Grava't la 1 i la 2: justifiquen tot el que ve a continuació.

Missatgeria asíncrona: cues productor-consumidor

L'alternativa a l'RPC síncron és no esperar: l'emissor deixa un missatge en una cua i continua amb la seva vida; un consumidor el processa quan pot.

flowchart LR
    PED[Servei Comandes] -- "missatge: PrepararComanda" --> Q[(Cua de cuina)]
    Q --> C1[Consumidor cuina 1]
    Q --> C2[Consumidor cuina 2]

És el patró productor-consumidor: cada missatge el processa un sol consumidor (els dos treballadors de cuina competeixen pels missatges). Avantatges davant de l'RPC:

  • Desacoblament temporal: si cuina està caiguda cinc minuts, les comandes esperen a la cua en lloc de perdre's.
  • Anivellament de càrrega: el pic de les 21:00 s'encua; els consumidors el processen al seu ritme (i pots afegir-hi consumidors).
  • El preu: el productor no sap quan (ni si) s'ha processat el seu missatge — la resposta, si n'hi ha, arriba com un altre missatge.

Avís important: aquest mateix patró existeix dins d'un procés amb fils i BlockingQueue; aquesta versió local és matèria de la propera lliçó, 06-04.

Pub-sub i brokers: Observer a escala de xarxa

A 04-08 vam deixar una porta oberta cap al pub-sub; aquí es creua. En publish-subscribe, l'emissor publica esdeveniments en un topic i tots els subscriptors en reben una còpia. És exactament la intenció de l'Observer — notificar interessats desconeguts sense acoblar-s'hi — amb dues diferències:

  • Entre subjecte i observadors s'hi interposa un broker (un intermediari dedicat), i
  • els observadors viuen en altres processos i poden estar caiguts quan es publica l'esdeveniment.
Observer (GoF, 04-08) Pub-sub distribuït
Registre comanda.subscriure(observador) en memòria Subscripció a un topic del broker
Lliurament Crida síncrona, mateix fil Asíncron, per xarxa, amb reintents
El subjecte coneix els observadors? Té la llista de referències No sap ni quants n'hi ha
Si l'observador falla L'excepció pot trencar la notificació El broker reintenta o aparca el missatge

Els dos brokers que has de conèixer, a nivell conceptual:

  • RabbitMQ: broker de cues clàssic; enruta missatges cap a cues i els esborra en confirmar-se. Bo per a treball distribuït i RPC asíncron.
  • Kafka: un log d'esdeveniments durador i particionat; els missatges no s'esborren en llegir-se, cada consumidor recorda la seva posició (offset) i pot rellegir el passat. Això l'emparella de manera natural amb l'Event Sourcing que vam veure a 06-01.

Garanties de lliurament i idempotència

Quantes vegades arriba un missatge? Les tres respostes possibles:

Garantia Significa Cost
At-most-once 0 o 1 vegades: s'envia i s'oblida Pots perdre missatges
At-least-once 1 o més vegades: es reintenta fins a confirmar Pots rebre duplicats
Exactly-once Exactament 1 vegada Molt car o impossible en general; se simula

L'opció pràctica és gairebé sempre at-least-once + consumidors idempotents: acceptes duplicats i fas que processar dues vegades sigui innocu. És la mateixa disciplina d'idempotència que vam aplicar als reintents de 06-02, ara al costat consumidor:

public void enRebre(EsdevenimentComandaCobrada esdeveniment) {
    // Idempotencia: si ja hem processat aquest id d'esdeveniment, ignorar el duplicat
    if (processats.existeix(esdeveniment.idEsdeveniment())) return;
    cuina.encuarComanda(esdeveniment.idComanda());
    processats.registrar(esdeveniment.idEsdeveniment()); // mateixa transaccio que l'efecte
}

L'"exactly-once" que anuncien algunes plataformes és, a la pràctica, at-least-once amb deduplicació automàtica: la idempotència no desapareix, només canvia de lloc.

El patró Outbox

Problema subtil i letal: el servei Comandes ha de (a) desar la comanda a la seva BD i (b) publicar ComandaConfirmada al broker. Són dos sistemes diferents: si desa i després cau abans de publicar, la comanda existeix però ningú no se n'assabenta (la cuina no la prepara mai). No hi ha cap transacció que abasti BD i broker.

El patró Transactional Outbox ho resol amb una sola transacció... a la BD:

  1. En la mateixa transacció que desa la comanda, s'insereix l'esdeveniment en una taula outbox.
  2. Un procés a part (relay) llegeix la taula outbox, publica els esdeveniments al broker i els marca com a enviats.
  3. Si el relay cau, reintenta: lliurament at-least-once, que ja sabem gestionar amb idempotència.

Atomicitat garantida per la BD, lliurament garantit pel reintent. Eines com Debezium fan de relay llegint directament el log de transaccions de la base de dades (Change Data Capture).

Arquitectura orientada a esdeveniments a PideYa

Ajuntem les peces: el viatge de la comanda, que a 05-02 era una seqüència de crides dins del monòlit, ara és una cadena d'esdeveniments:

sequenceDiagram
    participant P as Comandes
    participant B as Broker
    participant PA as Pagaments
    participant C as Cuina
    participant R as Repartiment
    participant N as Notificacions
    P->>B: publica ComandaConfirmada (via outbox)
    B->>PA: ComandaConfirmada
    PA->>B: publica ComandaCobrada
    B->>C: ComandaCobrada
    B->>N: ComandaCobrada (email al client)
    C->>B: publica ComandaLlesta
    B->>R: ComandaLlesta
    R->>B: publica ComandaLliurada
    B->>N: ComandaLliurada (notifica el client)
    B->>P: ComandaLliurada (tanca el cicle de vida)

Observa tres coses. Primera: ningú no crida ningú — cada servei publica fets i reacciona a fets, com els observadors de 04-08 però sense llista de subscriptors en memòria. Segona: afegir un servei d'estadístiques (el nostre vell ObservadorEstadistiques) és subscriure's als topics, sense tocar ni una línia dels altres. Tercera: això és una saga coreografiada de les de 06-02, vista des de la canonada.

Consistència eventual i el teorema CAP

En aquest món, quan el client pregunta "on és la meva comanda?", la pantalla pot anar uns segons per darrere de la realitat: l'esdeveniment ComandaLlesta existeix però encara no ha arribat a la vista de lectura. Això és la consistència eventual: si deixen de produir-se escriptures, totes les rèpliques acaben convergint — però mentrestant, es pot llegir el passat.

El teorema CAP (Brewer) explica per què no és un defecte arreglable, a nivell introductori: davant d'una Partició de xarxa (nodes incomunicats, i la fal·làcia 1 garanteix que passarà), un sistema distribuït ha de triar entre Consistència (rebutjar operacions per no divergir) i Availability/disponibilitat (respondre amb dades possiblement desactualitzades). PideYa tria per cas: el seguiment de la comanda prefereix disponibilitat (millor un estat amb 5 s de retard que un error), el cobrament prefereix consistència (millor rebutjar que cobrar dues vegades).

Resiliència complementària

Tres patrons que completen el circuit breaker i el retry de 06-02 (no els repetirem aquí):

  • Timeout: tota crida remota porta un límit d'espera explícit. Sense timeout, un servei penjat et penja a tu; el circuit breaker de 06-02 compta els timeouts com a fallades. Regla: el timeout del cridador ha de ser més gran que el del cridat, o cancel·laràs feina que anava a arribar.
  • Bulkhead (mampara, com els compartiments estancs d'un vaixell): aïlla recursos per dependència — un pool de connexions per cridar Pagaments i un altre de diferent per al Catàleg. Si Pagaments s'encalla, esgota el seu pool, no el de la resta de l'aplicació. És la intenció d'aïllar perquè la fallada no es propagui, germana del confinament que veurem en concurrència.
  • Dead Letter Queue (DLQ): quan un missatge falla repetidament (un esdeveniment malformat, un bug del consumidor), reintentar-lo per sempre bloqueja la cua. Després de N intents s'aparta a una cua de "cartes mortes" on un humà o un procés l'examina. Sense DLQ, un sol missatge verinós (poison message) pot aturar la cuina sencera de PideYa.

Errors Comuns i Consells

  • Creure's la transparència de l'RPC: tractar una crida remota com si fos local, sense timeout ni pla per a la fallada parcial. Tota crida que creua la xarxa necessita: timeout, política de reintent (només si és idempotent) i comportament definit quan falli.
  • Publicar al broker fora de la transacció: el bug silenciós que motiva l'Outbox. Si deses a BD i publiques després "a mà", tard o d'hora divergiran. Símptoma: comandes que existeixen però que cap altre servei no coneix.
  • Suposar ordre global d'esdeveniments: els brokers solen garantir ordre només per partició/clau. Dissenya els consumidors per tolerar ComandaLlesta arribant abans que ComandaCobrada, o particiona per id de comanda.
  • Ignorar els duplicats "perquè gairebé mai no passen": amb at-least-once, els duplicats són qüestió de temps. La deduplicació per id d'esdeveniment ha d'existir des del primer dia, i en la mateixa transacció que l'efecte.
  • Esdeveniments grassos: publicar la comanda completa amb la carta incrustada (fal·làcia 3). Publica fets amb ids i el mínim necessari; qui necessiti més, que consulti.
  • DLQ com a forat negre: aparcar missatges està bé; no monitorar la DLQ converteix fallades visibles en pèrdues invisibles. Alarma quan la DLQ creixi.

Exercicis

  1. Diagnòstic de fallada parcial. Comandes crida per RPC a Pagaments amb timeout de 3 s. La crida llança TimeoutException. Enumera els tres escenaris possibles sobre què va passar a Pagaments i què hauria de fer Comandes per reintentar amb seguretat.
  2. Cua o topic? Per a cada necessitat de PideYa, decideix entre cua productor-consumidor o topic pub-sub, i justifica-ho: (a) repartir les comandes de cuina entre les tres instàncies del servei de cuina; (b) avisar notificacions, estadístiques i facturació que una comanda s'ha lliurat; (c) processar els reemborsaments pendents un a un.
  3. Aplica l'Outbox. Escriu el pseudocodi (o Java esquemàtic) del mètode confirmarComanda del servei Comandes usant el patró Outbox, i del relay que publica. Indica quina garantia de lliurament en resulta i què ha de fer el consumidor per això.

Solucions

  1. Escenaris: (a) la petició no va arribar mai a Pagaments — no hi va haver cobrament; (b) va arribar i Pagaments va fallar a mitges — pot haver-hi cobrament o no; (c) Pagaments va cobrar correctament però la resposta es va perdre o va arribar tard — sí que hi va haver cobrament. Com que Comandes no pot distingir-los, el reintent només és segur si cobrar és idempotent: es reenvia amb la mateixa clau d'idempotència (id de la comanda) i Pagaments retorna el resultat ja registrat si el cobrament existia.
  2. (a) Cua: cada comanda de cuina l'ha de processar exactament un consumidor; les instàncies competeixen i a més anivellen la càrrega. (b) Topic pub-sub: un mateix fet interessa diversos subscriptors independents i demà se'n pot afegir un altre sense tocar l'emissor — Observer a escala de xarxa. (c) Cua: feina a repartir amb processament individual i possibilitat de DLQ per a reemborsaments que fallin repetidament.
  3. confirmarComanda: obrir transacció → repositoriComandes.desar(comanda) → outbox.inserir(new EsdevenimentComandaConfirmada(idEsdeveniment, idComanda)) → commit (les dues escriptures són atòmiques perquè són la mateixa BD). Relay (bucle o CDC): llegir files pendents d'outbox → publicar al broker → marcar com a enviades; si cau després de publicar i abans de marcar, en reiniciar torna a publicar. Resultat: at-least-once — el consumidor ha de deduplicar per idEsdeveniment (registrant els processats en la mateixa transacció que el seu efecte).

Conclusió

Hem recorregut la fontaneria que sosté la PideYa distribuïda: el proxy remot que disfressa la xarxa de mètode (i les vuit fal·làcies que castiguen qui s'ho creu), les cues que desacoblen en el temps, el pub-sub que porta la intenció de l'Observer a escala planetària, les garanties de lliurament amb el seu peatge d'idempotència, l'Outbox que lliga BD i broker, i la consistència eventual que CAP fa inevitable. Tot això tracta de coordinar processos separats per una xarxa. Però queda un front més proper i més traïdor: dins de cadascun d'aquests serveis hi ha fils compartint memòria, competint pel mateix estat a nanosegons de distància. Aquell synchronized i aquell double-checked locking que vam passar de puntetes al Singleton per fi s'explicaran de debò: Patrons de Concurrència.

Curs de Patrons de Disseny de Programari

Mòdul 1: Introducció als Patrons de Disseny

Mòdul 2: Patrons Creacionals

Mòdul 3: Patrons Estructurals

Mòdul 4: Patrons de Comportament

Mòdul 5: Aplicació de Patrons de Disseny

Mòdul 6: Patrons de Disseny Avançats

Mòdul 7: Recursos Addicionals i Conclusió

© Copyright 2026. Tots els drets reservats