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
- Què són els microserveis i què no
- Les cinc característiques que els defineixen
- Com tallar les fronteres: Domain-Driven Design lleuger
- Errors de tall
- Dades: una base de dades per servei i les seves conseqüències
- CQRS: el panell de comandes d'un productor i la vista
productes_vista - Comunicació: síncrona, asíncrona, APIs, gateway, BFF i mesh
- Patrons de migració: avaluació del pla d'estrangulament
- Organització: equips, Conway i plataforma interna
- Quan no utilitzar microserveis i el retorn al monòlit modular
- Registrar les decisions: l'ADR-001
- Errors Comuns i Consells
- Exercicis
- Conclusió
- 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.
- 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.
- 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:
- 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.
- Taxa de canvi: el que canvia a ritmes diferents convé separar-ho.
pagamentscanvia poc i amb auditoria;catalegcanvia diverses vegades al dia en campanya. Ajuntar-los imposa al catàleg el ritme de pagaments (símptoma 3 de 01-06). - 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.
- 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).
- 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.
- 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.
- CQRS: el panell de comandes d'un productor i la vista
productes_vista
productes_vistaEl 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à:
comandesrep "crear comanda", executa la saga, escriu a Cassandra i publica acomandes.esdevenimentsa 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 acomanda.creada,pagament.confirmat,pagament.rebutjati als esdeveniments d'assignació derepartiment, i manté una taulacomandes_per_productora 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_vistala creacatalegquan el productor publica el producte (ambdisponible = FALSE); la projecció només l'actualitza. Si arriba un esdeveniment d'estoc d'un producte encara no publicat, l'UPDATEno afecta cap fila i s'ignora sense error: no és cap problema perquè, quan es publiqui,catalegdemanarà l'estoc inicial perConsultarEstoc(02-03). versio_estoc < data_msés la defensa contra el desordre: amb 6 particions acomandes.esdevenimentsi 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 delSeguimentRepartidorde 01-05.- El
commitde l'offset va després de la transacció: si el procés mor a mig camí, l'esdeveniment es reprocessa imissatges_processatsel descarta. unitats_aproxporta "aprox" al nom a propòsit: és una decisió de llenguatge ubic. Ningú acatalegno ha d'usar aquest número per decidir si es pot vendre; això ésReservarEstocainventariamb 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.
- 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.
- 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.
- 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:
- 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.
- 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. - Contractes com a acords entre equips: canviar
inventari.protoo l'esquema d'estoc.actualitzatés una negociació amb els consumidors, verificada per les proves de contracte de 07-06, no una decisió unilateral. - 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.
- 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.
- 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 TABLEtrenca el veí. Vistes materialitzades per esdeveniments o composició a l'API; mai unSELECTcreuat. - 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.pyper 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-lintero 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:
comandestria 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_comandesa Cassandra, 3 nodes, RF=3, taulescomandes_per_clienticomandes_per_id(desnormalitzades per consulta), escriptures ambQUORUMi lectures ambLOCAL_QUORUMper a l'estat de la comanda iONEper a llistats; taulasagasal 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
JOINni 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
- Conceptes Bàsics de Sistemes Distribuïts
- Models de Sistemes Distribuïts
- Avantatges i Desafiaments dels Sistemes Distribuïts
- Les Fal·làcies de la Computació Distribuïda
- Temps, Rellotges i Ordenació d'Esdeveniments
- Del Monòlit a la Plataforma Distribuïda: el Cas Quilòmetre Zero
Mòdul 2: Comunicació en Sistemes Distribuïts
- Protocols de Comunicació
- RPC i RMI
- gRPC i Serialització de Dades
- Missatgeria i Cues de Missatges
- Patrons de Comunicació Asíncrona
Mòdul 3: Consistència i Replicació
- Models de Consistència
- El Teorema CAP i PACELC
- Algorismes de Consens
- Replicació de Dades
- Transaccions Distribuïdes i Sagues
Mòdul 4: Emmagatzematge Distribuït
- Particionament de Dades i Hashing Consistent
- Sistemes de Fitxers Distribuïts
- Emmagatzematge d'Objectes
- Bases de Dades Distribuïdes
- Memòries Cau Distribuïdes
Mòdul 5: Computació Distribuïda
- Models de Computació Distribuïda
- MapReduce i Hadoop
- Spark i Computació en Memòria
- Processament de Fluxos de Dades
- Planificació de Treballs i Pipelines de Dades
Mòdul 6: Seguretat en Sistemes Distribuïts
- Autenticació i Autorització
- Xifratge i Protecció de Dades
- Gestió d'Identitats
- Seguretat entre Serveis: mTLS i Gestió de Secrets
- Passarel·les d'API, Limitació de Taxa i Auditoria
Mòdul 7: Monitoratge i Manteniment
- Monitoratge de Sistemes Distribuïts
- Logs Centralitzats i Traçabilitat Distribuïda
- Gestió de Fallades i Recuperació
- Patrons de Resiliència: Timeouts, Reintents i Circuit Breaker
- Automatització i Orquestració
- Proves en Sistemes Distribuïts i Enginyeria del Caos
