La lliçó anterior va tancar el Mòdul 7 amb una pregunta pendent des del Mòdul 1: quan el monòlit Python+PostgreSQL es va partir en cataleg, comandes, inventari, pagaments, repartiment i analitica, què va fer que el resultat fos un sistema i no una col·lecció de sis programes que es criden per xarxa? Set mòduls han construït les peces (gRPC i Kafka, sagues i rèpliques, Cassandra i Redis, Spark i Flink, Keycloak i Vault, Prometheus i Kubernetes), però cap no ha explicat el criteri amb què es va decidir on tallar, quines dades posseeix cada servei, per què cataleg no consulta la base de dades d'inventari encara que tècnicament podria, ni què es paga per cadascuna d'aquestes decisions. Aquesta lliçó mira Quilòmetre Zero des de dalt, com a arquitectura de microserveis: què són (i què no), com es tracen les fronteres amb un Domain-Driven Design lleuger, com es resolen les consultes que travessen serveis quan cadascun és propietari de les seves dades, com es tria entre comunicació síncrona i asíncrona, com es migra des d'un monòlit sense aturar el negoci, com s'organitzen els equips i, sobretot, quan no convé fer-ho. Acaba amb el format que deixa escrites aquestes decisions: l'ADR. El temps real (08-02), el núvol (08-03) i serverless (08-04) queden per a les lliçons següents.

Contingut

  1. Què són els microserveis i què no
  2. Les cinc característiques que els defineixen
  3. Com tallar les fronteres: Domain-Driven Design lleuger
  4. Errors de tall
  5. Dades: una base de dades per servei i les seves conseqüències
  6. CQRS: el panell de comandes d'un productor i la vista productes_vista
  7. Comunicació: síncrona, asíncrona, APIs, gateway, BFF i mesh
  8. Patrons de migració: avaluació del pla d'estrangulament
  9. Organització: equips, Conway i plataforma interna
  10. Quan no utilitzar microserveis i el retorn al monòlit modular
  11. Registrar les decisions: l'ADR-001
  12. Errors Comuns i Consells
  13. Exercicis
  14. Conclusió

  1. Què són els microserveis i què no

A 01-02 es van presentar els "serveis/microserveis" com un model d'arquitectura més, al costat de client-servidor, capes i P2P, i es va remetre aquí. La definició operativa és aquesta: una arquitectura de microserveis descompon una aplicació en serveis petits, cadascun alineat amb una capacitat de negoci, que es despleguen de manera independent i es comuniquen per xarxa mitjançant contractes explícits. El que la distingeix no és la mida ni la tecnologia, sinó tres propietats que es reforcen mútuament: independència de desplegament, propietat de les dades i fronteres traçades pel negoci. Convé situar-la davant de les seves dues veïnes, amb les quals sovint es confon.

Aspecte Monòlit modular SOA clàssica Microserveis
Unitat de desplegament Una (tot el procés) Poques, grans, compartint un bus corporatiu (ESB) Moltes, petites, una per capacitat
Fronteres Paquets/mòduls dins del mateix codi; es travessen amb crides a funció Serveis de gra gruixut, sovint per sistema (CRM, ERP) Per capacitat de negoci (bounded context)
Dades Una base de dades compartida Bases compartides entre serveis; l'ESB transforma Una base de dades per servei; res compartit
Comunicació Crides en procés Bus centralitzat amb lògica d'orquestració i transformació "Canonades ximples, extrems llestos": HTTP/gRPC, esdeveniments; la lògica als serveis
Consistència Transaccions ACID locals Transaccions distribuïdes (2PC, 03-05) Consistència eventual, sagues
Equips Un o diversos sobre el mateix codi Per sistema o per capa Un equip propietari de cada servei, d'extrem a extrem
Escalat Tot o res (còpies del procés) Per servei, però amb l'ESB com a coll d'ampolla Per servei, independent
Fallada Un error tomba el procés sencer L'ESB és un punt únic Aïllada per servei (si està ben fet)
Quan encaixa Equips petits, domini encara inestable, latència crítica Integració de sistemes heretats heterogenis Diversos equips, dominis clars, necessitat d'escalat i desplegament diferenciats

Dos aclariments importants. El primer: el monòlit modular no és l'enemic. El monòlit de Quilòmetre Zero a 01-06 ja tenia sis paquets per domini, i aquesta modularitat és el que va fer possible l'extracció; un monòlit amb mòduls ben separats i una sola base de dades és una arquitectura legítima i, per a moltes empreses, la millor. El segon: la SOA dels anys 2000 va fracassar no per la idea de serveis sinó per l'ESB (Enterprise Service Bus), que concentrava lògica, transformacions i orquestració en un component central que acabava sent un nou monòlit, propietat d'un equip d'integració que tothom esperava. Kafka, a Quilòmetre Zero, és deliberadament el contrari: un log que no transforma ni decideix res (02-04); tota la intel·ligència és als productors i consumidors.

  1. Les cinc característiques que els defineixen

Les cinc propietats següents són l'examen que cada servei de Quilòmetre Zero ha d'aprovar. Quan una falla, el sistema s'acosta al monòlit distribuït, la pitjor combinació possible: la complexitat de la xarxa sense la independència que la justificava.

Característica Què significa Com es compleix a Quilòmetre Zero Com es trenca
Un servei = una capacitat de negoci El servei fa alguna cosa que el negoci reconeix pel seu nom: "reservar estoc", "cobrar", "assignar repartiment" inventari és el que un operador anomena "l'estoc"; repartiment és el que Jordi Sala anomena "la flota" Serveis per capa tècnica (servei-base-de-dades, servei-validacions)
Propietari de les seves dades Només el servei llegeix i escriu el seu emmagatzematge; els altres passen per la seva API o pels seus esdeveniments km0_inventari només el toca inventari; cataleg sap l'estoc per estoc.actualitzat (04-05) Un JOIN d'analitica contra les taules de comandes; una taula productes compartida
Desplegament independent Es pot publicar una versió nova sense coordinar-se amb ningú ni desplegar ningú més comandes 1.15.0 va sortir en canary (07-05) sense que inventari canviés Un canvi d'inventari que obliga a redesplegar comandes el mateix dia
Equip propietari Un equip decideix, construeix, desplega i opera el servei (you build it, you run it) L'equip de repartiment porta repartiment i en fa la guàrdia (07-03) Un "equip de backend" que toca els sis serveis
Fallades aïllades Un servei caigut o lent degrada la seva funció, no la plataforma pagaments lent obre el circuit (07-04) i la comanda queda en "pagament pendent"; el catàleg continua Crides síncrones encadenades sense timeout: el símptoma 2 de 01-06

Fixeu-vos que cap característica no parla de mida en línies de codi. "Micro" és una desafortunada tria de nom: la mida correcta és la que permet complir les cinc propietats amb un equip de mida raonable (la regla informal d'"un equip que s'alimenta amb dues pizzes"). comandes és probablement el servei més gran de Quilòmetre Zero, amb el seu orquestrador de sagues, la seva outbox i el seu repositori de Cassandra, i està bé així: partir-lo en comandes-creacio i comandes-consulta no afegiria independència, només crides de xarxa.

  1. Com tallar les fronteres: Domain-Driven Design lleuger

La pregunta més difícil d'una arquitectura de microserveis no és tècnica: és on tallar. Tallar malament produeix serveis que canvien sempre alhora, que es criden en cadena per a qualsevol operació i que comparteixen dades per la porta del darrere. L'eina conceptual més útil és el Domain-Driven Design (DDD), del qual aquí només prenem tres idees, suficients per traçar fronteres sense adoptar tota la seva metodologia.

Llenguatge ubic

Cada domini té un vocabulari que el negoci i el codi han de compartir paraula per paraula. Un producte al catàleg té nom, descripció, fotos i preu; per a l'inventari és una referència amb unitats disponibles i reservades en un mercat; per al repartiment és un paquet amb pes i necessitat de fred. És el mateix formatge-curat, però amb tres significats diferents, i forçar una única classe Producte amb els camps dels tres és el que produeix les taules amb 80 columnes del monòlit. El llenguatge ubic és el senyal per detectar fronteres: allà on una mateixa paraula canvia de significat, hi ha un límit de context.

Bounded contexts

Un bounded context (context delimitat) és l'àmbit en què un model i el seu llenguatge són vàlids i coherents. A dins, un terme significa una sola cosa; a fora, en pot significar una altra. Un microservei ben tallat coincideix amb un bounded context (o, de vegades, amb un conjunt petit d'ells que un equip posseeix junts). Els sis contextos de Quilòmetre Zero, amb el terme que dona nom al seu model central:

Context Concepte central (agregat) Termes propis El que no li pertany
cataleg Producte publicat fitxa, foto, preu de venda, productor, categoria, "disponible" (indicador, no quantitat) La quantitat exacta en estoc; el preu pagat en una comanda concreta
comandes Comanda línia, estat (creada, pagada, rebutjada, lliurada), import, saga Què hi ha en estoc; com es cobra; on és la furgoneta
inventari Referència per mercat unitats disponibles/reservades, reserva amb caducitat, llindar d'alerta La descripció o la foto; el preu
pagaments Cobrament passarel·la, autorització, reemborsament, Idempotency-Key Les línies de la comanda (només l'import i la referència)
repartiment Ruta i posició repartidor, paquet, franja horària, posició, assignació El contingut de la comanda més enllà de pes i fred
analitica Fet de venda vendes per dia/mercat/productor, previsió, alerta d'estoc (lectura) Res transaccional: només llegeix esdeveniments

Mapa de contextos

El mapa de contextos dibuixa les relacions entre bounded contexts i, per a cadascuna, qui mana sobre el contracte. Les relacions habituals són: client-proveïdor (el consumidor negocia el contracte amb el proveïdor: comandes demana a inventari un ReservarEstoc), conformista (el consumidor accepta el model del proveïdor tal com és: analitica consumeix els esdeveniments tal com arriben), capa anticorrupció (el consumidor tradueix el model aliè al seu per no contaminar-se: pagaments davant de la passarel·la externa) i llenguatge publicat (un contracte explícit i versionat que tothom coneix: els esdeveniments de comandes.esdeveniments amb el seu embolcall id_esdeveniment/tipus/versio/data_ms/origen/dades de 02-04).

flowchart LR
    subgraph Web[Clients]
        NAV["Navegador de l'Anna"]
        APP[App repartidors]
        PAN[Panell productors]
    end
    NAV --> GW[Kong<br/>06-05]
    APP --> GW
    PAN --> GW
    GW --> CAT[cataleg<br/>Producte publicat]
    GW --> PED[comandes<br/>Comanda]
    GW --> REP[repartiment<br/>Ruta i posició]
    PED -- "gRPC ReservarEstoc<br/>client-proveïdor" --> INV[inventari<br/>Referència per mercat]
    PED -- "gRPC Cobrar<br/>client-proveïdor" --> PAG[pagaments<br/>Cobrament]
    PAG -- "ACL" --> PAS[(Passarella externa)]
    PED -. "comanda.creada / pagament.confirmat<br/>llenguatge publicat" .-> K[(Kafka<br/>comandes.esdeveniments)]
    INV -. "estoc.actualitzat / estoc.reservat" .-> K
    K -. conformista .-> CAT
    K -. conformista .-> REP
    K -. conformista .-> ANA[analitica<br/>Fet de venda]
    REP -. "repartiment.posicions" .-> K2[(Kafka<br/>repartiment.*)]
    K2 -.-> ANA

Les fletxes contínues són crides síncrones i dibuixen les dependències en temps d'execució: si inventari cau, comandes no pot confirmar (però sí acceptar, com es va veure a 03-05). Les discontínues són esdeveniments i no creen dependència en temps d'execució: cataleg continua funcionant amb l'última vista que tingui encara que Kafka estigui caigut. Un mapa amb moltes fletxes contínues és el primer senyal d'un tall discutible.

Heurístiques per decidir on tallar

Quan el llenguatge no n'hi ha prou per decidir, quatre heurístiques ajuden, i convé aplicar-les totes perquè de vegades es contradiuen:

  1. Cohesió: el que canvia junt ha de viure junt. La reserva d'estoc i l'alliberament de la reserva són la mateixa capacitat; separar-les en dos serveis obligaria a coordinar cada canvi de regles en dos llocs.
  2. Taxa de canvi: el que canvia a ritmes diferents convé separar-ho. pagaments canvia poc i amb auditoria; cataleg canvia diverses vegades al dia en campanya. Ajuntar-los imposa al catàleg el ritme de pagaments (símptoma 3 de 01-06).
  3. Dades que canvien juntes en la mateixa transacció: si dues entitats necessiten actualitzar-se atòmicament amb freqüència, separar-les obliga a una saga per operació. Les unitats disponibles i les reservades d'una referència s'actualitzen sempre juntes: mateix servei, mateixa fila. La comanda i l'estoc s'actualitzen junts només en confirmar: aquí sí que compensa la saga perquè la resta del temps són independents.
  4. Equips: una frontera de servei que travessa un equip genera dos serveis que es despleguen sempre alhora; una que agrupa dos equips genera baralles pel mateix codi. La frontera ha de coincidir amb la propietat (apartat 9).

  1. Errors de tall

Els tres errors més freqüents tenen nom propi, i val la pena reconèixer-los perquè tots tres semblen "més microserveis" i són el contrari.

Serveis anèmics o serveis-entitat. Tallar per taula: servei-productes, servei-clients, servei-comandes, cadascun un CRUD sense lògica. Tota operació de negoci ("confirmar comanda") travessa quatre o cinc d'ells en cadena síncrona, i la lògica acaba en un "servei orquestrador" que és el monòlit de sempre amb latència de xarxa. La prova: si el servei només té endpoints GET/POST/PUT/DELETE sobre un recurs i cap verb de negoci (reservar, cobrar, assignar), és una taula amb API, no una capacitat.

Nanoserveis. Tallar tan fi que el cost de la comunicació supera el de la funció: un servei per calcular l'IVA, un altre per formatar adreces. Cadascun necessita desplegament, monitoratge, contracte, guàrdia. Quilòmetre Zero va decidir a 01-06 que comandes inclou el càlcul de l'import i no un servei-preus, i a l'exercici 1 d'aquella lliçó que els cupons simples viuen a comandes, no en un servei-promocions.

Monòlit distribuït. El resultat de trencar qualsevol de les cinc característiques de l'apartat 2: serveis que comparteixen base de dades, que s'han de desplegar junts perquè comparteixen un model de dades o una llibreria interna amb lògica de negoci, o que es criden en cadena síncrona per a tot. Senyals mesurables: cada release toca més de tres repositoris; un canvi d'esquema d'una taula exigeix coordinar dos equips; la traça d'una petició senzilla (07-02) mostra més de cinc salts síncrons.

  1. Dades: una base de dades per servei i les seves conseqüències

El principi "cada servei és propietari de les seves dades" (01-06, principi 1) és el que més costa d'acceptar perquè renuncia a l'eina més potent del monòlit: el JOIN. Al monòlit, el panell de la Formatgeria Montblanc es resolia amb una consulta:

-- Al monòlit: una sola consulta sobre una sola base de dades
SELECT p.id, p.data, p.estat, l.producte, l.unitats, pr.nom,
       r.repartidor, r.lliurament_previst
FROM comandes p
JOIN linies l     ON l.comanda_id = p.id
JOIN productes pr ON pr.slug = l.producte
LEFT JOIN repartiments r ON r.comanda_id = p.id
WHERE pr.productor = 'formatgeria-montblanc'
ORDER BY p.data DESC;

Amb comandes a Cassandra (04-04), cataleg a PostgreSQL i repartiment al seu propi emmagatzematge, aquesta consulta ja no existeix. Hi ha tres maneres de reconstruir-la, i triar bé entre elles és una decisió d'arquitectura tan important com el tall dels serveis.

Tècnica Com funciona Quan encaixa Cost
Composició a l'API Un component (el BFF, o el mateix servei que atén la petició) crida diversos serveis i uneix les respostes en memòria Poques crides, dades petites, necessitat de frescor immediata Latència = suma de crides (o el màxim si són paral·leles); la disponibilitat és el producte de les disponibilitats; sense paginació ni filtre creuat eficients
Vista materialitzada per esdeveniments El servei que necessita la consulta se subscriu als esdeveniments dels altres i manté una còpia desnormalitzada a la seva pròpia base, amb la forma exacta de la consulta Consultes freqüents, filtres i ordenacions creuades, tolerància a segons de retard Consistència eventual; emmagatzematge duplicat; codi de projecció per mantenir
CQRS Separar formalment el model d'escriptura (ordres, validacions, agregats) del model de lectura (vistes construïdes a partir dels esdeveniments), fins i tot en magatzems diferents Càrregues de lectura i escriptura molt diferents, moltes vistes diferents dels mateixos fets Dos models per mantenir; la lectura no veu l'escriptura a l'instant

Les tres tècniques es recolzen en peces que el curs ja té: la composició usa els clients gRPC amb timeouts i circuit breaker (02-03, 07-04); les vistes materialitzades usen el consumidor idempotent i l'outbox de 02-05; i CQRS generalitza les vistes materialitzades.

Consistència eventual com a norma. A 03-05 la saga va substituir la transacció global per passos locals amb compensacions; la conseqüència per a les consultes és que, durant uns mil·lisegons o segons, la comanda P-2026-000125 existeix a comandes però encara no a la vista del productor. En una arquitectura de microserveis això no és un defecte que calgui corregir sinó l'estat normal, i el disseny ho assumeix: la interfície mostra "actualitzat fa 3 s", els tests accepten la finestra (07-06), i l'única pregunta que es fa per a cada dada és "quin retard tolera qui la llegeix?". La taula de 03-01 de models de consistència per dada és la que es revisa aquí: forta només per a l'estoc disponible i l'estat del cobrament; eventual per a tota la resta.

  1. CQRS: el panell de comandes d'un productor i la vista productes_vista

El panell del productor com a problema de CQRS

Marta Puig, de la Formatgeria Montblanc, obre el seu panell i espera veure les seves comandes dels últims 30 dies amb producte, unitats, estat i hora prevista de lliurament, filtrables per mercat i ordenables per data. Els fets viuen en tres serveis: la comanda i les seves línies a comandes, el nom del producte a cataleg, l'assignació i l'hora prevista a repartiment. Per composició a l'API caldria llegir totes les comandes (Cassandra no filtra per productor: la seva clau és el client, 04-04), consultar cataleg per cada producte i repartiment per cada comanda, i ordenar en memòria: inviable a partir d'uns centenars de comandes.

CQRS ho resol separant els dos costats:

  • El costat d'ordres (commands) continua com està: comandes rep "crear comanda", executa la saga, escriu a Cassandra i publica a comandes.esdeveniments a través de l'outbox. És el model optimitzat per escriure correctament: validacions, idempotència, estats.
  • El costat de consultes és un consumidor nou dins de comandes (o un servei de consulta propi, si creix) que se subscriu a comanda.creada, pagament.confirmat, pagament.rebutjat i als esdeveniments d'assignació de repartiment, i manté una taula comandes_per_productor a PostgreSQL amb exactament les columnes i l'índex que el panell necessita. És el model optimitzat per llegir ràpid.
flowchart LR
    PAN[Panell de la Marta] -- "1. POST /comandes (ordre)" --> W[comandes: costat d'escriptura<br/>saga, outbox]
    W --> C[(Cassandra<br/>km0_comandes)]
    W -. "comanda.creada, pagament.confirmat" .-> K[(Kafka<br/>comandes.esdeveniments)]
    REP[repartiment] -. "repartiment.assignat" .-> K
    K --> P[Projecció<br/>consumidor idempotent]
    P --> V[(PostgreSQL<br/>comandes_per_productor)]
    PAN -- "2. GET /productors/formatgeria-montblanc/comandes (consulta)" --> R[comandes: costat de lectura]
    R --> V

Els dos costats comparteixen el nom comandes i l'equip, però no l'esquema ni la base de dades: l'escriptura no sap que la vista existeix, i la vista es pot reconstruir des de zero rellegint el tòpic des del principi (la retenció de Kafka de 02-04 és el que ho permet), cosa que també és la manera d'afegir una columna nova al panell sense migració.

Codi: la vista productes_vista a cataleg

El mateix patró, en la seva forma més simple, és el que cataleg usa per saber si un producte està disponible sense consultar inventari. A 04-05 l'esdeveniment estoc.actualitzat invalidava la memòria cau de Redis; aquí, a més, alimenta una taula desnormalitzada a la base de dades de cataleg, de manera que la fitxa del producte i els llistats filtrables per disponibilitat se serveixen amb una sola consulta local.

-- km0/sql/cataleg/productes_vista.sql
-- Vista materialitzada "a mà": la manté el consumidor, no PostgreSQL.
CREATE TABLE IF NOT EXISTS productes_vista (
    slug            TEXT NOT NULL,
    mercat          TEXT NOT NULL,
    nom             TEXT NOT NULL,           -- copiat del model de cataleg
    productor       TEXT NOT NULL,
    preu_cents      INTEGER NOT NULL,
    disponible      BOOLEAN NOT NULL DEFAULT FALSE,
    unitats_aprox   INTEGER NOT NULL DEFAULT 0,  -- "aprox": és eventual per disseny
    versio_estoc    BIGINT NOT NULL DEFAULT 0,   -- últim data_ms aplicat d'inventari
    actualitzat_en  TIMESTAMPTZ NOT NULL DEFAULT now(),
    PRIMARY KEY (slug, mercat)
);
CREATE INDEX IF NOT EXISTS idx_productes_vista_disp ON productes_vista (mercat, disponible, productor);
# km0/serveis/cataleg/projeccio_estoc.py
"""Projecció CQRS: manté productes_vista a partir d'estoc.actualitzat.

Reutilitza el consumidor idempotent de 02-05 (taula missatges_processats) i la
invalidació de Redis de 04-05. Només hi afegeix l'escriptura a la vista.
"""
import json
import psycopg
from confluent_kafka import Consumer
from serveis.comu import logs, metriques
from serveis.cataleg.cache import invalidar_producte   # 04-05

log = logs.obtenir("cataleg.projeccio")
LLINDAR_DISPONIBLE = 1   # una unitat lliure ja compta com a "disponible"


def aplicar(conn: psycopg.Connection, esdeveniment: dict) -> None:
    """Aplica un estoc.actualitzat a la vista. Idempotent i tolerant al desordre."""
    d = esdeveniment["dades"]
    with conn.transaction():
        # 1. Deduplicar per id_esdeveniment (02-05): si ja s'ha aplicat, sortir sense tocar res.
        ja = conn.execute(
            "INSERT INTO missatges_processats (id_missatge, consumidor) VALUES (%s, 'cataleg.projeccio') ON CONFLICT DO NOTHING RETURNING 1",
            (esdeveniment["id_esdeveniment"],)).fetchone()
        if ja is None:
            metriques.comptador("km0_projeccio_duplicats_total").inc()
            return
        # 2. Escriure només si l'esdeveniment és més recent que el que ja s'ha aplicat (01-05: ordenació).
        #    Si arriba un esdeveniment antic per un reintent tardà, la condició el descarta.
        n = conn.execute(
            """
            UPDATE productes_vista
               SET disponible = %(disp)s, unitats_aprox = %(uts)s,
                   versio_estoc = %(v)s, actualitzat_en = now()
             WHERE slug = %(slug)s AND mercat = %(mercat)s AND versio_estoc < %(v)s
            """,
            {"slug": d["producte"], "mercat": d["mercat"],
             "uts": d["disponible"], "disp": d["disponible"] >= LLINDAR_DISPONIBLE,
             "v": esdeveniment["data_ms"]}).rowcount
    if n:
        invalidar_producte(d["producte"], d["mercat"])   # la memòria cau de 04-05 continua sent vàlida
    log.info("vista actualitzada", producte=d["producte"], mercat=d["mercat"], aplicat=bool(n))


def bucle(conn: psycopg.Connection, consumidor: Consumer) -> None:
    consumidor.subscribe(["comandes.esdeveniments"])
    while True:
        msg = consumidor.poll(1.0)
        if msg is None or msg.error():
            continue
        esdeveniment = json.loads(msg.value())
        if esdeveniment["tipus"] != "estoc.actualitzat":
            continue            # aquesta projecció ignora els altres tipus
        aplicar(conn, esdeveniment)
        consumidor.commit(msg)  # ack després d'escriure: at-least-once + idempotència (02-05)

Explicació de les decisions que el codi conté:

  • La fila de productes_vista la crea cataleg quan el productor publica el producte (amb disponible = FALSE); la projecció només l'actualitza. Si arriba un esdeveniment d'estoc d'un producte encara no publicat, l'UPDATE no afecta cap fila i s'ignora sense error: no és cap problema perquè, quan es publiqui, cataleg demanarà l'estoc inicial per ConsultarEstoc (02-03).
  • versio_estoc < data_ms és la defensa contra el desordre: amb 6 particions a comandes.esdeveniments i clau = id de comanda, dos esdeveniments del mateix producte poden anar a particions diferents i arribar invertits. Sense aquesta condició, un esdeveniment vell trepitjaria un de nou. És el mateix raonament del SeguimentRepartidor de 01-05.
  • El commit de l'offset va després de la transacció: si el procés mor a mig camí, l'esdeveniment es reprocessa i missatges_processats el descarta.
  • unitats_aprox porta "aprox" al nom a propòsit: és una decisió de llenguatge ubic. Ningú a cataleg no ha d'usar aquest número per decidir si es pot vendre; això és ReservarEstoc a inventari amb consistència forta.

Codi: el bounded context com a paquet amb API pública

L'altra meitat de "propietari de les seves dades" és que la resta del codi no pugui saltar-se la frontera. Dins d'un servei Python, la convenció de Quilòmetre Zero és que cada bounded context és un paquet amb un únic mòdul públic, api.py, i tota la resta privada. Qui importi serveis.inventari.repositori des de comandes està cometent el mateix error que un JOIN creuat.

# km0/serveis/inventari/api.py
"""API pública del bounded context `inventari`.

És l'ÚNIC mòdul d'aquest paquet que altres contextos poden importar (en el
mateix procés, durant la migració) o exposar per gRPC (contractes/inventari.proto).
Tot el que no és aquí és detall d'implementació i pot canviar sense avís.
"""
from dataclasses import dataclass
from serveis.inventari import _domini, _repositori, _esdeveniments   # privats: prefix _

__all__ = ["Reserva", "EstocInsuficient", "reservar_estoc", "alliberar_reserva", "consultar_estoc"]


@dataclass(frozen=True)
class Reserva:
    id_reserva: str
    comanda_id: str
    caduca_en_s: int


class EstocInsuficient(Exception):
    """Error de negoci, no tècnic: la saga de 03-05 el tracta com a fallada permanent."""


def reservar_estoc(id_reserva: str, comanda_id: str, mercat: str, linies: list[dict]) -> Reserva:
    """Reserva unitats de diverses referències en un mercat, atòmicament.

    Idempotent per id_reserva: repetir la crida retorna la mateixa Reserva.
    Publica estoc.reservat i estoc.actualitzat via outbox en la mateixa transacció.
    """
    with _repositori.transaccio() as tx:
        if (existent := _repositori.cercar_reserva(tx, id_reserva)):
            return existent
        for linia in linies:
            ref = _repositori.bloquejar_referencia(tx, linia["producte"], mercat)  # SELECT ... FOR UPDATE
            _domini.reservar(ref, linia["unitats"])          # llança EstocInsuficient
            _repositori.desar_referencia(tx, ref)
            _esdeveniments.publicar(tx, "estoc.actualitzat", {"producte": ref.slug, "mercat": mercat,
                                                             "disponible": ref.disponibles})
        reserva = _repositori.crear_reserva(tx, id_reserva, comanda_id, linies, caduca_en_s=900)
        _esdeveniments.publicar(tx, "estoc.reservat", {"comanda_id": comanda_id, "id_reserva": id_reserva})
        return reserva


def alliberar_reserva(id_reserva: str) -> None:
    """Compensació de la saga (C2 a 03-05). Idempotent: alliberar dues vegades no duplica estoc."""
    with _repositori.transaccio() as tx:
        reserva = _repositori.cercar_reserva(tx, id_reserva)
        if reserva is None or reserva.alliberada:
            return
        for linia in reserva.linies:
            ref = _repositori.bloquejar_referencia(tx, linia["producte"], reserva.mercat)
            _domini.alliberar(ref, linia["unitats"])
            _repositori.desar_referencia(tx, ref)
            _esdeveniments.publicar(tx, "estoc.actualitzat", {"producte": ref.slug, "mercat": reserva.mercat,
                                                             "disponible": ref.disponibles})
        _repositori.marcar_alliberada(tx, id_reserva)
        _esdeveniments.publicar(tx, "estoc.alliberat", {"comanda_id": reserva.comanda_id, "id_reserva": id_reserva})


def consultar_estoc(producte: str, mercat: str) -> int:
    """Lectura amb consistència forta (va al primari de Patroni). Per a la vista de cataleg
    NO s'usa això: s'usa la projecció per esdeveniments."""
    return _repositori.llegir_disponibles(producte, mercat)

El servidor gRPC de 02-03 (ReservarEstoc, ConsultarEstoc) no fa res més que traduir missatges protobuf a aquestes tres funcions, i el ServeiInterceptor de 06-04 decideix qui pot cridar cadascuna. L'avantatge de tenir el context com a paquet amb API és que durant la migració (apartat 8) el monòlit va poder importar serveis.inventari.api en procés abans que existís el servei remot, i el canvi a gRPC no va tocar els que el cridaven.

  1. Comunicació: síncrona, asíncrona, APIs, gateway, BFF i mesh

Síncrona o asíncrona, cas per cas

El Mòdul 2 va donar les eines (gRPC a 02-03, Kafka i RabbitMQ a 02-04, patrons asíncrons a 02-05). El que correspon a aquesta lliçó és el criteri per triar, que es resumeix en una pregunta: el que crida necessita la resposta per continuar, i la necessita ara?

Situació Tria Raó Exemple a Quilòmetre Zero
El resultat decideix el pas següent i ha de ser fort Síncrona (gRPC) La saga no pot avançar sense saber si hi ha estoc comandes → inventari.ReservarEstoc
Una persona espera la resposta en pantalla Síncrona, amb timeout curt i degradació L'Anna no espera més de 500 ms (SLO de 07-01) cataleg servint la fitxa; comandes acceptant la comanda
Diversos interessats en un mateix fet Asíncrona (esdeveniment) El productor no ha de conèixer els consumidors pagament.confirmat → repartiment, analitica, cataleg, Lambda de factures (08-04)
El receptor pot estar caigut sense que importi Asíncrona Desacoblament temporal analitica consumint-ho tot
Feina llarga o per lots Asíncrona (cua o esdeveniment) No bloquejar qui crida Generar la factura; recalcular previsions
Alt volum amb ordre per clau Asíncrona (Kafka amb clau) Ordre per partició i replay repartiment.posicions
Notificació a l'exterior amb confirmació del tercer Síncrona cap enfora, asíncrona cap endins La passarel·la respon síncrona; el resultat es propaga com a esdeveniment pagaments → passarel·la; després pagament.confirmat

La regla pràctica de Quilòmetre Zero, fixada a l'exercici 2 de 01-06 i confirmada aquí: síncron només quan la resposta governa la decisió següent; tota la resta, esdeveniments. Cada crida síncrona afegeix una dependència de disponibilitat (la disponibilitat de la cadena és el producte de la de les seves baules) i de latència (la suma), i per això el mapa de contextos de l'apartat 3 només té dues fletxes contínues entre serveis.

API pública davant d'API interna

No totes les interfícies són iguals. Una API pública (la que consumeix el navegador de l'Anna o l'app de repartidors, i en el futur tercers) és un compromís a llarg termini: es versiona, es documenta (OpenAPI), es protegeix al gateway, es limita per taxa i es manté compatible durant mesos. Una API interna (gRPC entre comandes i inventari) té pocs consumidors coneguts, canvia amb més llibertat i la seva compatibilitat es verifica amb les proves de contracte de 07-06. Confondre-les porta o a exposar detalls interns (l'id_reserva no li interessa a l'Anna) o a burocratitzar canvis interns.

Versionat d'APIs

Tota API pública canvia. La disciplina té dos nivells. Els canvis compatibles (afegir un camp opcional a la resposta, afegir un endpoint, acceptar un paràmetre nou amb valor per defecte) no necessiten versió nova: el client antic ignora el que no coneix. Els canvis incompatibles (canviar el nom d'un camp o treure'l, canviar-ne el tipus o la semàntica, canviar codis d'error) exigeixen una versió nova coexistint amb l'antiga durant un període anunciat. Quilòmetre Zero versiona a la ruta (/api/v1, /api/v2), que és l'opció més visible i la que Kong encamina sense ambigüitat (06-05).

# km0/serveis/comandes/api_http.py (fragment): v1 i v2 servides pel mateix codi
from fastapi import FastAPI, Header
from pydantic import BaseModel

app = FastAPI()


class ComandaV1(BaseModel):
    id: str
    estat: str
    total: float                # en euros, amb decimals: la decisió original


class ComandaV2(BaseModel):
    id: str
    estat: str
    total_cents: int            # canvi INCOMPATIBLE: tipus i semàntica diferents
    moneda: str = "EUR"
    lliurament_previst: str | None = None   # camp NOU opcional: hauria estat compatible a v1


def _carregar(comanda_id: str) -> dict:
    ...  # repositori de Cassandra (04-04); el model intern desa total_cents


@app.get("/api/v1/comandes/{comanda_id}", response_model=ComandaV1, deprecated=True)
def comanda_v1(comanda_id: str):
    p = _carregar(comanda_id)
    # Adaptador: el model intern ja és v2; v1 es DERIVA d'ell, mai a l'inrevés.
    return ComandaV1(id=p["id"], estat=p["estat"], total=p["total_cents"] / 100)


@app.get("/api/v2/comandes/{comanda_id}", response_model=ComandaV2)
def comanda_v2(comanda_id: str):
    p = _carregar(comanda_id)
    return ComandaV2(id=p["id"], estat=p["estat"], total_cents=p["total_cents"],
                     lliurament_previst=p.get("lliurament_previst"))

Tres detalls importen més que el codi: la versió antiga es marca deprecated i s'anuncia una data de retirada (capçalera Sunset a la resposta i avís als consumidors registrats a Kong); es mesura qui continua usant v1 (la mètrica km0_http_requests_total{ruta="/api/v1/..."} de 07-01 diu quan es pot apagar); i el model intern mai no es manté en dues formes: v1 és una traducció del model actual. Per als esdeveniments, el versionat ja es va tractar a 02-05 (versio a l'embolcall, compatibilitat cap enrere a l'esquema).

API gateway i BFF

El gateway de 06-05 és la vora de l'API pública: encaminament, JWT, rate limiting, X-Request-Id. Des del punt de vista arquitectònic, la seva funció és que els clients coneguin un domini i no sis serveis, i que les preocupacions transversals visquin en un sol lloc. El que no ha de fer és compondre respostes: quan un client necessita dades de diversos serveis en una pantalla, el BFF (Backend for Frontend) és el lloc, un per tipus de client, propietat de l'equip d'aquest client. La pantalla d'inici de l'app de repartidors (ruta del dia, propers paquets, incidències) és el cas previst a 06-05 i que el projecte final (08-05) situa a l'arquitectura.

Service mesh: quan val la pena

A 06-04 es va veure el mesh com a manera de tenir mTLS sense codi, i a 07-05 com a sidecar que dona timeouts, reintents, outlier detection i telemetria RED per configuració; totes dues van remetre aquí la decisió. El criteri és de nombre i heterogeneïtat: amb sis serveis, tots en Python i amb serveis/comu/{resiliencia,metriques,traces}.py compartits, el mesh afegeix 1-2 ms per salt, memòria per sidecar i un pla de control per operar, a canvi de poca cosa que la llibreria comuna no doni ja. Compensa quan apareixen serveis en altres llenguatges (un recomanacions en Go o Java que no pot usar la llibreria Python), quan el nombre de serveis supera la desena i la uniformitat de polítiques esdevé un problema de govern, o quan la seguretat exigeix mTLS obligatori auditable (PeerAuthentication: STRICT) sense refiar-se que cada equip el configuri bé. Quilòmetre Zero l'adopta a l'arquitectura final (08-05) per aquesta última raó i perquè el cost d'operar-lo l'assumeix la plataforma interna (apartat 9), no els equips de producte.

  1. Patrons de migració: avaluació del pla d'estrangulament

A 01-06 es va fixar un pla d'estrangulament (strangler fig) en cinc fases. Ara, amb totes les peces construïdes, és el moment d'avaluar-lo fase per fase: què es va extreure, amb quin patró, i què se'n va aprendre. Abans, els quatre patrons que es van usar.

  • Strangler fig: es col·loca una façana davant del monòlit (a Quilòmetre Zero, Kong), s'implementa una capacitat en un servei nou i s'hi redirigeix només aquella ruta; el monòlit perd responsabilitats fins a quedar buit. En cap moment hi ha dos sistemes sencers per mantenir.
  • Anti-corruption layer (ACL): una capa de traducció entre el model nou i l'antic (o un d'extern), perquè el model del servei nou no hereti els defectes del monòlit. És la relació "capa anticorrupció" del mapa de contextos.
  • Branch by abstraction: dins del monòlit, s'introdueix una interfície (InventariPort) amb dues implementacions, la local i la remota, seleccionable per configuració; es prova la remota amb un percentatge del trànsit i es retira la local quan està validada. Permet migrar sense branques de codi de llarga durada.
  • Migració de dades amb doble escriptura i CDC: per moure una taula del monòlit a la base del servei nou sense aturada: primer es copia l'històric; després el monòlit escriu a totes dues (doble escriptura, amb la nova com a secundària) o, millor, un procés de Change Data Capture (Debezium llegint el WAL de PostgreSQL, el mateix WAL de 03-04) replica cada canvi al nou magatzem; es verifica la igualtat; s'inverteix la direcció (el servei nou és primari i el monòlit en llegeix); i s'apaga la còpia antiga.
Fase (01-06) Què es va extreure Patrons aplicats Lliçons on es va construir Què se'n va aprendre
1 cataleg amb Redis i km0-fotos Strangler fig per la ruta /api/v1/cataleg; dades copiades en fred (només lectures, sense doble escriptura) 04-03, 04-05, 06-05 Començar per lectures treu risc: el retorn al monòlit era canviar una ruta a Kong. El símptoma 1 va desaparèixer abans de tocar res transaccional
2 pagaments aïllat ACL davant de la passarel·la; branch by abstraction (PagamentsPort local/remot); Idempotency-Key 02-05, 07-04 L'ACL va evitar arrossegar al servei nou el model de "cobrament dins de la transacció de la comanda". El circuit breaker va resoldre el símptoma 2 fins i tot abans d'extreure comandes
3 repartiment amb Kafka i temps real Nou tòpic repartiment.posicions; la taula posicions va deixar d'escriure's a PostgreSQL (sense migrar l'històric: dades que caduquen) 02-04, 04-04, 05-04, 08-02 Les dades de sèrie temporal no es migren, es deixen d'escriure al lloc equivocat. Va ser la fase que va introduir el bus, del qual després es va penjar tot
4 comandes i inventari Doble escriptura + CDC per a comandes (a Cassandra) i per a inventari (a km0_inventari); saga substituint la transacció; outbox 03-04, 03-05, 04-04, 02-05 La fase més llarga i arriscada: dues migracions de dades amb verificació d'igualtat durant tres setmanes. Es van descobrir un 0,4 % de comandes amb línies òrfenes al monòlit, que la migració va netejar
5 analitica sobre Kafka i HDFS Consumidor conformista de tots els tòpics; llac /km0/esdeveniments; DAG km0_vendes_diaries 04-02, 05-02, 05-03, 05-05 L'última perquè depenia que tot fos ja en esdeveniments. Les consultes nocturnes contra producció van desaparèixer i el símptoma 5 amb elles
Transversal Seguretat, observabilitat, Kubernetes Introduïts des de la fase 1 (Kong, Prometheus, traces) i ampliats a cada fase Mòduls 6 i 7 El que no es va posar des del principi (les traces van arribar a la fase 2) va costar el doble de posar després

Una observació sobre l'ordre: el pla de 01-06 va posar repartiment (fase 3) abans que comandes (fase 4) encara que el símptoma 3 (desplegaments) fos el que els equips sentien cada setmana. Va ser una decisió correcta per una raó que aleshores no es va fer explícita: repartiment va obligar a muntar Kafka, i sense Kafka la fase 4 no hauria pogut usar l'outbox ni les vistes materialitzades. L'ordre d'una migració el dicta la infraestructura que cada fase deixa a la següent, no només el dolor que alleuja.

  1. Organització: equips, Conway i plataforma interna

La llei de Conway (1967) diu que un sistema reflecteix l'estructura de comunicació de l'organització que el construeix. És una llei empírica, però contrastada: si dos equips s'han de posar d'acord per a cada canvi, apareixerà una interfície entre els seus components; si un equip posseeix dos serveis, tendiran a acoblar-se perquè ningú no paga el cost de mantenir-los separats. La conseqüència pràctica, de vegades anomenada maniobra inversa de Conway, és que l'organització es dissenya per obtenir l'arquitectura desitjada: a 01-06 Quilòmetre Zero tenia quatre equips (catàleg i productors, comandes i pagaments, repartiment, dades), i les fronteres de servei van seguir aquestes línies. comandes i pagaments són dos serveis del mateix equip, i de fet són els dos que més es criden síncronament: Conway en acció.

Quatre regles d'organització que l'arquitectura necessita per sostenir-se:

  1. Equips alineats a flux, propietaris d'una capacitat de negoci d'extrem a extrem (codi, dades, desplegament, guàrdia), en lloc d'equips per capa (frontend, backend, base de dades), que converteixen cada funcionalitat en un projecte de tres equips.
  2. Un equip de plataforma interna que ofereix Kubernetes, el mesh, Kafka, l'observabilitat, el pipeline de desplegament i les llibreries serveis/comu/ com un producte amb usuaris (els equips de producte), amb documentació i SLO propis. Sense ell, cada equip reinventa la infraestructura, i la uniformitat que fa possible operar sis serveis es perd. A Quilòmetre Zero és l'antic equip de "dades" ampliat amb les persones que van muntar Kubernetes a 07-05.
  3. Contractes com a acords entre equips: canviar inventari.proto o l'esquema d'estoc.actualitzat és una negociació amb els consumidors, verificada per les proves de contracte de 07-06, no una decisió unilateral.
  4. Guàrdia per servei, no una guàrdia central: l'equip que desplega és el que rep l'alerta del seu SLO (07-01, 07-03). És el que alinea els incentius per fer serveis operables.

  1. Quan no utilitzar microserveis i el retorn al monòlit modular

Quilòmetre Zero necessitava microserveis perquè tenia cinc símptomes mesurables i quatre equips. La majoria de les aplicacions no els tenen, i en elles una arquitectura de microserveis és un impost sense contrapartida: cada frontera de xarxa costa latència, fallades parcials, consistència eventual, contractes, desplegaments i guàrdies.

Senyal Què indica Què fer en lloc seu
Un sol equip (menys de 8-10 persones) No hi ha conflicte de desplegament per resoldre Monòlit modular amb paquets per domini i API interna (api.py) per paquet
El domini encara no s'entén bé (producte nou, pivots freqüents) Les fronteres que es tallin avui seran les equivocades demà, i moure codi entre serveis és car Monòlit modular; tallar quan el llenguatge s'estabilitzi
Tot el trànsit cap en dues o tres rèpliques del procés No hi ha necessitat d'escalat diferenciat Escalar el monòlit horitzontalment; memòria cau i rèpliques de lectura
La majoria d'operacions necessiten transaccions sobre diversos "dominis" Cada frontera obligaria a una saga Mantenir juntes les dades que canvien juntes
No hi ha capacitat d'operar Kubernetes, Kafka, observabilitat distribuïda La plataforma consumirà l'equip Un procés, una base de dades, un desplegament senzill; PaaS
Latència extremadament sensible (trading, jocs) Cada salt de xarxa costa Mòduls en procés
L'objectiu és "modernitzar" sense símptoma concret Complexitat sense justificació (error de 01-06: "triar tecnologies abans que problemes") Escriure els símptomes; si no n'hi ha, no migrar

El retorn al monòlit modular no és un fracàs, i hi ha casos públics d'empreses que han reagrupat serveis que es cridaven en cadena per a tot. Els senyals per reagrupar dos serveis són els inversos dels de tallar: canvien sempre junts, tots els seus canvis exigeixen desplegar-los alhora, la traça de qualsevol operació els travessa sempre tots dos, o els posseeix el mateix equip i no tenen necessitats d'escalat diferents. A Quilòmetre Zero, si d'aquí a dos anys pagaments no tingués més lògica que l'adaptador a la passarel·la, seria candidat a tornar dins de comandes com a paquet amb el seu api.py: l'ACL i la idempotència es conservarien; la frontera de xarxa, no.

  1. Registrar les decisions: l'ADR-001

A 01-06 es va donar el consell de mantenir un registre de decisions d'arquitectura. Un ADR (Architecture Decision Record) és un document breu, numerat, immutable un cop acceptat (si la decisió canvia, s'escriu un altre ADR que el substitueix), amb quatre parts: context, decisió, alternatives considerades i conseqüències. El seu valor no és en la decisió, que sol conèixer-se, sinó en el context i les conseqüències, que són el que s'oblida i el que d'aquí a un any permetrà saber si la decisió continua sent vàlida. Aquest és el primer de Quilòmetre Zero, que el projecte final (08-05) completarà fins a l'ADR-006.

# ADR-001: Extreure `inventari` del monòlit com a servei amb base de dades pròpia

- **Estat**: Acceptat (fase 4 del pla d'estrangulament)
- **Data**: 2026-03-10
- **Decisors**: equip de comandes i pagaments, equip de plataforma
- **Substitueix**: —

## Context

L'estoc viu a la taula `productes.unitats` del monòlit i es descompta dins de la
transacció de confirmació de comanda, que abasta també el cobrament a la passarel·la externa.
Símptomes (01-06): (2) la lentitud de la passarel·la manté bloquejos d'estoc durant 30 s i
esgota les connexions de PostgreSQL; (3) l'equip de comandes no pot desplegar canvis a
les regles de reserva sense arrossegar el catàleg i el repartiment. L'estoc exigeix consistència
forta per mercat (no vendre la mateixa peça dues vegades), i només `comandes` el modifica.
`cataleg` únicament necessita saber si un producte està disponible, amb un retard tolerable
de segons.

## Decisió

Extreure `inventari` com a bounded context propi, amb base de dades `km0_inventari`
(PostgreSQL amb Patroni, 03-04/07-03), API gRPC `contractes/inventari.proto`
(`ReservarEstoc`, `ConsultarEstoc`, `ObservarCanvis`) i esdeveniments `estoc.actualitzat`,
`estoc.reservat`, `estoc.alliberat` publicats via outbox. La reserva d'estoc passa a ser
un pas de la saga de comanda (03-05), amb caducitat de 15 minuts i compensació
`alliberar_reserva`. Migració de dades amb CDC (Debezium sobre el WAL del monòlit) i
verificació d'igualtat durant tres setmanes abans d'invertir la direcció.

## Alternatives considerades

1. **Deixar l'estoc a `comandes`** (mateixa base, sense frontera). Rebutjada: acobla les
   regles de reserva al cicle de desplegament de comandes i no permet a `cataleg` conèixer
   la disponibilitat sense consultar la base de comandes.
2. **Estoc a Cassandra al costat de les comandes**. Rebutjada: el descompte d'estoc necessita
   comparar-i-actualitzar amb consistència forta per fila; Cassandra ho ofereix amb
   transaccions lleugeres (Paxos) a un cost alt i `inventari` no necessita la seva escala
   d'escriptura (03-02, 04-04).
3. **Transacció distribuïda 2PC entre `comandes` i `inventari`**. Rebutjada: bloquejos
   mentre el coordinador decideix, i la passarel·la externa no participa en 2PC (03-05).

## Conseqüències

- (+) `pagaments` lent ja no bloqueja estoc: la reserva té caducitat i la saga compensa.
- (+) Les regles de reserva es despleguen de manera independent.
- (+) `cataleg` obté la disponibilitat per esdeveniments (vista `productes_vista`).
- (−) Consistència eventual entre estoc real i catàleg: finestra de segons en què
  un producte esgotat apareix disponible; s'accepta i es gestiona amb "sense estoc" a la
  confirmació.
- (−) Una crida síncrona més al camí crític de confirmar comanda (p99 +12 ms);
  l'SLO de 500 ms continua complint-se.
- (−) Nou component amb guàrdia: Patroni, backups i PITR de `km0_inventari` (07-03).
- (−) Els informes que feien `JOIN` d'estoc amb comandes es reescriuen sobre el llac
  d'esdeveniments (fase 5).

Els ADR viuen al repositori (km0/docs/adr/), es revisen en pull request com el codi i s'enllacen des del tiquet que va originar el canvi. Un ADR que només diu "usem Kafka" sense context ni conseqüències negatives no serveix: les conseqüències negatives són la part que més valor té.

Errors Comuns i Consells

  • Tallar per taula o per capa tècnica. Produeix serveis-entitat i cadenes síncrones. Talla per capacitat de negoci; si el servei no té un verb de negoci a la seva API, replanteja'l.
  • Compartir la base de dades "només per a lectures". És la manera més silenciosa de crear un monòlit distribuït: el primer ALTER TABLE trenca el veí. Vistes materialitzades per esdeveniments o composició a l'API; mai un SELECT creuat.
  • Convertir cada consulta creuada en una crida síncrona. La disponibilitat es multiplica i la latència se suma. Si la consulta és freqüent i tolera segons de retard, és una projecció CQRS.
  • Versionar l'API trencant l'anterior sense període de coexistència. Tota versió nova conviu amb l'antiga, marcada deprecated, amb data de retirada i amb una mètrica que digui qui la continua usant.
  • Migrar en "big bang". Estrangular per rutes, començar per lectures, migrar dades amb CDC i verificació, i conservar sempre la marxa enrere (un canvi de ruta a Kong).
  • Ignorar Conway. Fronteres de servei que no coincideixen amb equips s'acoblen o es barallen. Dissenya l'organització juntament amb l'arquitectura.
  • Adoptar microserveis sense símptomes. Escriu els símptomes abans de tallar. Si no n'hi ha, el monòlit modular amb api.py per paquet és la resposta correcta, i és reversible.
  • Consell: cada bounded context amb un sol mòdul públic; una prova d'arquitectura (amb import-linter o similar) que falli si algú importa un mòdul privat d'un altre context.
  • Consell: escriu l'ADR abans de la decisió, no després: obliga a enumerar alternatives i conseqüències negatives quan encara es pot canviar d'idea.

Exercicis

Exercici 1: tallar un domini nou

Quilòmetre Zero vol afegir valoracions: l'Anna puntua d'1 a 5 cada producte que ha rebut i escriu un comentari; el productor respon; la fitxa del catàleg mostra la mitjana i els tres últims comentaris; el panell del productor mostra les seves valoracions pendents de resposta. Només es pot valorar un producte d'una comanda lliurada. Decideix: (a) és un bounded context propi o part de cataleg o de comandes? Justifica-ho amb les heurístiques de l'apartat 3. (b) Quina comunicació té amb els altres contextos (síncrona/asíncrona, i en quina direcció)? (c) Com obté la fitxa del catàleg la mitjana de valoracions sense cridar síncronament el nou context?

Exercici 2: detectar el monòlit distribuït

Un equip proposa aquesta implementació de "confirmar comanda": el BFF crida comandes, que crida clients (servei nou que exposa la taula d'usuaris) per validar l'adreça, després preus (nou, calcula l'import amb IVA) i inventari, pagaments i repartiment en seqüència, tot síncron; analitica consulta cada nit les taules de comandes amb un usuari de només lectura; i preus comparteix amb cataleg la llibreria km0-model amb les classes Producte i Preu. Enumera cada violació de les cinc característiques de l'apartat 2, anomena-la (servei-entitat, nanoservei, monòlit distribuït, base compartida...) i proposa la correcció amb les tècniques de la lliçó.

Exercici 3: escriure un ADR

Escriu l'ADR-002, "Cassandra per a comandes", amb les quatre parts (context, decisió, alternatives rebutjades, conseqüències positives i negatives), recolzant-te en el que es va decidir a 03-02 i 04-04. Inclou-hi almenys tres conseqüències negatives.

Solucions

Exercici 1.

(a) Context propi, valoracions. Llenguatge: "valoració", "mitjana", "resposta del productor" no existeixen en cap altre context, i "producte" aquí és només un identificador. Cohesió: les regles (una valoració per línia lliurada, moderació, resposta única) canvien juntes i no amb el catàleg. Taxa de canvi: és una funcionalitat nova que evolucionarà ràpid (moderació, fotos, filtres), diferent del ritme de comandes. Dades que canvien juntes: la valoració no s'escriu mai en la mateixa transacció que la comanda ni que la fitxa. Equip: encaixa en el de catàleg i productors, que pot posseir dos contextos. Posar-ho a cataleg acoblaria la moderació de comentaris als desplegaments del catàleg en campanya; a comandes contaminaria el context més crític amb lògica que no té res a veure amb la compra.

(b) Entrada: valoracions consumeix comanda.lliurada (esdeveniment de repartiment, asíncron) per saber quines línies es poden valorar: desa (client, comanda, producte, lliurat_en) a la seva pròpia base; així, en rebre "l'Anna valora formatge-curat de la P-2026-000124", ho comprova localment sense cridar comandes. Sortida: publica valoracio.creada i valoracio.resposta en un tòpic valoracions.esdeveniments. No hi ha cap crida síncrona entre serveis: l'única síncrona és del BFF/gateway a valoracions per crear i llistar.

(c) cataleg manté una projecció: consumeix valoracio.creada i actualitza a productes_vista (o una taula productes_valoracio) suma_punts, num_valoracions i una llista dels tres últims comentaris (JSON). La mitjana es calcula en llegir (suma/num) i tolera segons de retard. Amb el consumidor idempotent, una valoració reprocessada no compta dues vegades. És exactament la vista productes_vista de l'apartat 6 amb una altra font.

Exercici 2.

Element proposat Violació Nom Correcció
clients exposa la taula d'usuaris i comandes el crida per validar l'adreça Servei sense capacitat de negoci; crida síncrona per a una dada estable Servei-entitat L'adreça de lliurament viatja dins l'ordre de crear comanda (el client l'ha triat en pantalla); la identitat ja ve al JWT (06-01). comandes desa una còpia de l'adreça a la comanda (desnormalització legítima: l'adreça de la comanda no canvia si l'Anna es muda)
preus calcula l'import amb IVA en un servei a part Cost de xarxa per una funció pura Nanoservei Funció dins de comandes (la decisió de 01-06). Si les regles de preu creixessin (tarifes per mercat, promocions complexes), seria un context preus amb la seva pròpia lògica, però consultat per esdeveniment o amb preus publicats al catàleg
Cadena síncrona inventari → pagaments → repartiment repartiment no governa la confirmació; disponibilitat multiplicada Monòlit distribuït (acoblament temporal) inventari i pagaments síncrons dins de la saga (03-05); repartiment consumeix pagament.confirmat de manera asíncrona i publica repartiment.assignat
analitica amb usuari de només lectura sobre les taules de comandes Trenca "propietari de les seves dades"; acobla esquemes; càrrega nocturna en producció (símptoma 5) Base de dades compartida Consumidor conformista de comandes.esdeveniments cap al llac (05-05)
Llibreria km0-model compartida amb classes de domini Un canvi a Producte obliga a redesplegar tots dos; nega el llenguatge ubic (mateix Producte per a dos contextos) Monòlit distribuït (acoblament per llibreria) Cada context amb el seu propi model; el que es comparteix es limita a serveis/comu/ (mètriques, logs, traces, resiliència: infraestructura, no domini) i als contractes (.proto, esquemes d'esdeveniments), que es versionen

Exercici 3. Un ADR-002 acceptable:

  • Context. Les comandes creixen 1,2 M al mes en campanya; el patró d'accés és "comandes d'un client per data" i "comanda per id", sense consultes ad hoc; s'exigeix disponibilitat per acceptar comandes fins i tot amb un node caigut o una partició (03-02: comandes tria disponibilitat); PostgreSQL amb Patroni té un primari únic que limita l'escriptura i el failover del qual (30 s) suposaria rebutjar comandes en campanya.
  • Decisió. km0_comandes a Cassandra, 3 nodes, RF=3, taules comandes_per_client i comandes_per_id (desnormalitzades per consulta), escriptures amb QUORUM i lectures amb LOCAL_QUORUM per a l'estat de la comanda i ONE per a llistats; taula sagas al mateix keyspace.
  • Alternatives rebutjades. PostgreSQL particionat per client amb Citus (manté SQL però introdueix un coordinador i transaccions distribuïdes que el patró d'accés no necessita); MongoDB (els documents hi encaixen, però el model de rèplica amb primari únic té el mateix problema de failover); mantenir PostgreSQL i acceptar els 30 s d'indisponibilitat (rebutjada pel símptoma 1: els pics coincideixen amb quan més fa mal).
  • Conseqüències. (+) Escriptura lineal amb nodes; sense failover de primari; tolera un node caigut sense rebutjar comandes. (−) Sense JOIN ni consultes ad hoc: cada pregunta nova exigeix una taula nova o una vista CQRS (el panell del productor de l'apartat 6). (−) Consistència ajustable que el codi ha de triar a cada operació: un error de nivell és un bug silenciós. (−) Nova tecnologia per operar: compactacions, reparacions, snapshots (07-03), formació de l'equip. (−) Les transaccions lleugeres (Paxos) són cares: per això l'estoc no va aquí (ADR-001).

Conclusió

Una arquitectura de microserveis és un sistema, i no una col·lecció de serveis, quan cada servei compleix cinc propietats: representa una capacitat de negoci, és propietari de les seves dades, es desplega sol, el posseeix un equip i les seves fallades no es propaguen. Les fronteres es tracen allà on el llenguatge canvia de significat (bounded contexts), guiades per la cohesió, la taxa de canvi, les dades que canvien juntes i els equips, i el mapa de contextos de Quilòmetre Zero mostra el resultat: només dues fletxes síncrones (comandes → inventari, comandes → pagaments) i tota la resta per esdeveniments. Renunciar al JOIN obliga a compondre a l'API, a mantenir vistes materialitzades per esdeveniments o a separar formalment escriptura i lectura amb CQRS, com el panell de la Formatgeria Montblanc i la taula productes_vista, amb la consistència eventual com a estat normal i no com a defecte. La comunicació es tria cas per cas (síncrona només quan la resposta governa la decisió següent), les APIs públiques es versionen amb coexistència i mètriques d'ús, el gateway i el BFF separen el transversal de l'específic de cada client, i el mesh compensa quan l'heterogeneïtat o l'exigència de mTLS obligatori superen el que una llibreria comuna pot donar. El pla d'estrangulament de 01-06 s'ha revisat fase per fase (strangler fig, ACL, branch by abstraction, CDC), la llei de Conway explica per què l'organització i l'arquitectura s'han de dissenyar juntes, la taula de senyals diu quan el monòlit modular és la resposta correcta, i l'ADR-001 deixa escrit per què inventari va sortir del monòlit i què es paga per això.

Queda una peça de la plataforma que aquest mapa només ha anomenat: la fletxa discontínua repartiment.posicions acaba a Kafka, però l'Anna no llegeix Kafka. Entre el tòpic i la seva pantalla hi ha un últim tram amb regles pròpies (navegadors, mòbils, dispositius en xarxes dolentes, milers de connexions obertes), i aquest tram, amb MQTT i WebSockets que 02-01 va deixar presentats, és la lliçó següent: Sistemes de Missatgeria en Temps Real.

Curs d'Arquitectures Distribuïdes

Mòdul 1: Introducció als Sistemes Distribuïts

Mòdul 2: Comunicació en Sistemes Distribuïts

Mòdul 3: Consistència i Replicació

Mòdul 4: Emmagatzematge Distribuït

Mòdul 5: Computació Distribuïda

Mòdul 6: Seguretat en Sistemes Distribuïts

Mòdul 7: Monitoratge i Manteniment

Mòdul 8: Casos d'Estudi i Aplicacions

© Copyright 2026. Tots els drets reservats