Les cinc lliçons anteriors han donat els fonaments: què és un sistema distribuït, amb quins models es descriu, què guanya i què perd qui distribueix, quines fal·làcies cal desterrar i per què el temps és un problema. Aquesta lliçó de tancament els posa a treballar sobre el cas que ens acompanyarà la resta del curs. Examinarem amb detall el monòlit de Quilòmetre Zero, els símptomes concrets que l'empenyen a canviar i l'arquitectura objectiu que anirem construint mòdul a mòdul.

És important entendre el caràcter d'aquesta lliçó: és un mapa, no el viatge. Aquí no s'explica cap tecnologia concreta; es decideix quines peces necessita la plataforma, per què i en quina lliçó del curs es desenvolupa cadascuna. També prepararem l'entorn de treball mínim (l'estructura de carpetes del projecte km0/ i un docker-compose.yml només amb PostgreSQL i l'aplicació Python) que serà el punt de partida sobre el qual s'aniran afegint serveis.

Contingut

  1. El monòlit de Quilòmetre Zero per dins
  2. Els símptomes que forcen l'evolució
  3. Principis de disseny per a la transició
  4. L'arquitectura objectiu
  5. Mapa de l'arquitectura al curs
  6. Un camí gradual, no un "big bang"
  7. Preparació de l'entorn de treball: el projecte km0/
  8. Errors comuns i consells
  9. Exercicis de disseny
  10. Conclusió

  1. El monòlit de Quilòmetre Zero per dins

Recordem el punt de partida (lliçó 01-01): una aplicació Python en un únic procés, una base de dades PostgreSQL i un únic servidor. Mirem-ho ara amb més detall, perquè per decidir com partir-lo cal entendre com està fet.

Mòduls interns

El codi està organitzat en sis mòduls Python, que es corresponen amb els dominis funcionals del negoci:

Mòdul Responsabilitat Taules principals Qui el fa servir
cataleg Productes, productors, categories, fotos, cerca productes, productors, categories Clients (moltes lectures), productors (poques escriptures)
comandes Cistell, creació i cicle de vida de la comanda comandes, linies_comanda Clients, atenció al client
inventari Estoc per producte i per productor, reserves estoc, moviments_estoc comandes, productors
pagaments Cobraments amb la passarel·la externa, reemborsaments pagaments, reemborsaments comandes
repartiment Assignació de repartidors, rutes, posició en temps real repartiments, repartidors, posicions Repartidors (app mòbil), clients (seguiment)
analitica Informes de vendes, productes més venuts, previsió de demanda Consultes sobre totes les anteriors Direcció, productors

Com interactuen

Les interaccions entre mòduls són crides a funcions Python, i les operacions que toquen diversos mòduls es recolzen en una única transacció de base de dades. El flux de confirmar una comanda n'és l'exemple més clar:

sequenceDiagram
    participant C as Client (Anna)
    participant P as comandes
    participant I as inventari
    participant PA as pagaments
    participant DB as PostgreSQL
    C->>P: confirmar_comanda(cistell)
    P->>DB: BEGIN
    P->>I: reservar_estoc(linies)
    I->>DB: UPDATE estoc ...
    P->>PA: cobrar(import, targeta)
    PA->>PA: crida a passarella externa (HTTPS)
    PA->>DB: INSERT pagament
    P->>DB: INSERT comanda
    P->>DB: COMMIT
    P-->>C: comanda confirmada

Fixa't en un detall que serà decisiu: la crida a la passarel·la de pagaments externa passa dins de la transacció de base de dades. Si la passarel·la triga 8 segons, la transacció (i els bloqueigs sobre les files d'estoc) duren 8 segons. I si la passarel·la falla, la transacció es desfà sencera, cosa que és correcta, però mentrestant ha estat bloquejant l'estoc per a tots els altres clients.

Desplegament

Un únic artefacte (un paquet Python) que s'instal·la al servidor amb un script. El desplegament implica aturar el procés, instal·lar i arrencar: uns 40 segons d'indisponibilitat total. Es desplega els dimarts i dijous a la nit, amb tots els canvis de tots els equips acumulats des del desplegament anterior.

  1. Els símptomes que forcen l'evolució

No hi ha una única raó per transformar Quilòmetre Zero, sinó cinc símptomes diferents, i cadascun empeny cap a una peça diferent de l'arquitectura objectiu. Convé identificar-los amb precisió perquè cada símptoma justifica una decisió concreta, i les decisions que no responen a cap símptoma són complexitat gratuïta.

Símptoma 1: els pics de campanya ho tomben tot

Ja ho vam veure: la Setmana de la Verema va multiplicar per 15 el trànsit i el servidor va caure del tot. L'anàlisi posterior va mostrar que el 92 % de les peticions eren lectures del catàleg (llistats, cerques, fitxes de producte amb fotos). La resta de la plataforma va caure per compartir procés i base de dades amb el catàleg.

Què demana: poder escalar el catàleg de manera independent, servir les lectures des d'una memòria cau i les fotos des d'un emmagatzematge a part, i aïllar les fallades perquè una sobrecàrrega del catàleg no afecti les comandes ni el repartiment.

Símptoma 2: una fallada de pagaments que ho va tombar tot

Un dimarts, la passarel·la de pagaments externa va començar a respondre amb 30 segons de retard. Cada confirmació de comanda mantenia una transacció oberta (i bloqueigs d'estoc) durant aquests 30 segons. En quatre minuts, PostgreSQL va esgotar el seu límit de connexions i tota la plataforma, incloses les pàgines del catàleg que no tenen res a veure amb pagaments, va deixar de respondre. Una fallada d'un proveïdor extern es va convertir en una caiguda total.

Què demana: que pagaments sigui un component aïllat amb els seus propis recursos, que la seva lentitud o la seva fallada es contingui (timeouts, degradació controlada), i que la confirmació de comanda no depengui d'una transacció única que abasta sistemes externs.

Símptoma 3: els equips es trepitgen en desplegar

Quilòmetre Zero ja té quatre equips (catàleg i productors, comandes i pagaments, repartiment, dades). Cada desplegament porta els canvis de tots, així que un error al mòdul de repartiment obliga a revertir també les millores del catàleg. L'equip de repartiment vol desplegar diverses vegades al dia; el de pagaments, subjecte a auditoria, vol cicles llargs i controlats. El resultat és que ningú no desplega amb la freqüència que necessita, i cada desplegament de dimarts és un esdeveniment tens.

Què demana: unitats de desplegament independents per domini, amb els seus propis cicles, proves i responsables, i una plataforma de desplegament que permeti fer-ho sense aturada.

Símptoma 4: la telemetria de repartidors en temps real

Quilòmetre Zero ha passat de 12 a 140 repartidors, i cadascun envia la seva posició cada 5 segons: 28 posicions per segon, 2,4 milions al dia, que s'insereixen a la taula posicions de la mateixa base de dades que gestiona les comandes. La taula creix 800 MB al dia i les insercions competeixen amb les transaccions de les comandes. A més, els clients volen veure el repartidor moure's pel mapa en temps real, cosa que amb el model petició-resposta obliga que l'app pregunti cada pocs segons, multiplicant la càrrega.

Què demana: un canal de comunicació diferent per a fluxos d'esdeveniments d'alta freqüència (no una taula relacional), comunicació asíncrona i bidireccional amb les apps, i un emmagatzematge adequat per a sèries temporals de gran volum.

Símptoma 5: l'analítica de vendes ofega la producció

Cada nit, el mòdul analitica executa consultes pesades sobre les taules de comandes i estoc per generar informes. Les consultes triguen 3 hores i, durant aquest temps, la base de dades va lenta per a tothom. Els productors demanen informes més rics (previsió de demanda per temporada, comparatives entre ciutats) que l'equip de dades no s'atreveix a implementar perquè cada consulta nova posa en risc la producció.

Què demana: separar la càrrega analítica de la transaccional, amb una còpia de les dades en un sistema dissenyat per al processament massiu, alimentada pels esdeveniments que produeixen els altres serveis.

  1. Principis de disseny per a la transició

Abans de dibuixar l'arquitectura objectiu, fixem els principis que es deriven del mòdul i que guiaran totes les decisions:

  1. Cada servei és propietari de les seves dades. Cap servei no accedeix directament a les taules d'un altre. Si analitica necessita les comandes, les rep com a esdeveniments o a través de l'API de comandes. És el que fa possible el desplegament i l'escalat independents (símptomes 1 i 3), i el que obliga a renunciar a la transacció única (que se substitueix per les tècniques del Mòdul 3).
  2. Aïllament de fallades. Una fallada o una sobrecàrrega en un servei no s'ha de propagar als altres (símptomes 1 i 2). Això exigeix recursos separats, timeouts a tota crida remota (lliçó 01-04) i degradació controlada: si repartiment cau, es pot continuar comprant; si pagaments cau, es pot continuar mirant el catàleg.
  3. Síncron només quan la resposta fa falta ara. Comprovar l'estoc abans de confirmar una comanda necessita resposta immediata: síncron. Avisar analitica que s'ha creat una comanda no la necessita: asíncron, mitjançant esdeveniments. Cada crida síncrona que s'evita és una frontera de xarxa menys a la cadena de disponibilitat (lliçó 01-03).
  4. Assumir les fal·làcies com a certes a l'inrevés. La xarxa fallarà, tindrà latència, no serà segura, la topologia canviarà. Tota comunicació entre serveis porta timeout, reintents idempotents, xifratge i descobriment dinàmic.
  5. Observabilitat des del principi. Amb sis serveis, sense mètriques, logs correlacionats i traces distribuïdes no hi ha manera de saber què passa. No és un afegit posterior: és part del disseny.
  6. Evolució gradual. No es reescriu tot alhora. S'extreu un servei, es comprova, s'extreu el següent. El monòlit continua funcionant durant tota la transició.

  1. L'arquitectura objectiu

Amb els símptomes i els principis sobre la taula, aquesta és la plataforma que el curs construirà:

flowchart TB
    subgraph Clients
        WEB[Navegador web]
        APP[App mobil clients]
        REP[App repartidors]
    end

    GW[API Gateway<br/>autenticació, limitació de taxa]
    WEB --> GW
    APP --> GW
    REP -->|MQTT / WebSocket| RT[Servidor temps real]

    subgraph Serveis["Serveis (Kubernetes)"]
        CAT[cataleg]
        PED[comandes]
        INV[inventari]
        PAG[pagaments]
        REPA[repartiment]
        ANA[analitica]
    end

    GW --> CAT
    GW --> PED
    GW --> REPA
    PED -->|gRPC síncron| INV
    PED -->|gRPC síncron| PAG
    RT --> REPA

    subgraph Bus["Bus d'esdeveniments (Kafka)"]
        T1[(comandes.esdeveniments)]
        T2[(repartiment.posicions)]
        T3[(inventari.esdeveniments)]
    end
    PED -.->|ComandaCreada, ComandaPagada| T1
    INV -.->|EstocActualitzat| T3
    REPA -.->|PosicioActualitzada| T2
    T1 -.-> ANA
    T2 -.-> ANA
    T3 -.-> CAT
    T1 -.-> REPA

    subgraph Dades["Emmagatzematge per servei"]
        PGC[(PostgreSQL<br/>cataleg)]
        RED[(Redis<br/>memòria cau catàleg)]
        OBJ[(Magatzem d'objectes<br/>fotos)]
        CAS[(Cassandra<br/>comandes)]
        PGI[(PostgreSQL replicat<br/>inventari)]
        PGP[(PostgreSQL<br/>pagaments)]
        TS[(Cassandra<br/>posicions)]
        DL[(Data lake + Spark/Flink<br/>analitica)]
    end
    CAT --> PGC
    CAT --> RED
    CAT --> OBJ
    PED --> CAS
    INV --> PGI
    PAG --> PGP
    PAG -->|HTTPS| EXT[Passarella de pagaments externa]
    REPA --> TS
    ANA --> DL

    subgraph Transversal["Transversal"]
        OBS[Observabilitat<br/>Prometheus, Grafana, OpenTelemetry]
        SEC[Seguretat<br/>OIDC, mTLS, secrets]
    end

Recorrem les decisions, cadascuna vinculada al seu símptoma:

Els sis serveis

Els límits dels serveis coincideixen amb els mòduls del monòlit, i no és casualitat: els mòduls ja reflectien els dominis del negoci i els equips. Un bon monòlit modular és la millor preparació per a una arquitectura de serveis. Cada servei té el seu propi desplegament, el seu propi escalat i el seu propi emmagatzematge (símptomes 1, 2 i 3).

Comunicació síncrona: gRPC

Per a les interaccions que necessiten resposta immediata (comandesinventari per reservar estoc; comandespagaments per cobrar), es farà servir RPC amb gRPC: crides tipades, amb contracte explícit, eficients en serialització. Tota crida porta timeout i les operacions amb efectes són idempotents (Mòdul 2).

Comunicació asíncrona: esdeveniments per Kafka

Per a tot el que no necessita resposta immediata, els serveis publiquen esdeveniments en un bus (Kafka): ComandaCreada, ComandaPagada, EstocActualitzat, PosicioActualitzada. Qui hi estigui interessat s'hi subscriu: analitica ho consumeix tot; cataleg consumeix EstocActualitzat per mostrar disponibilitat; repartiment consumeix ComandaPagada per planificar el lliurament. Això desacobla els serveis en el temps (si analitica està caiguda, els esdeveniments esperen al bus) i resol el símptoma 5 (les dades arriben a l'analítica sense consultar la base de dades de producció) i part del 4 (les posicions són un flux d'esdeveniments, no files d'una taula transaccional).

Emmagatzematge per servei

Cada servei tria l'emmagatzematge que encaixa millor amb la seva càrrega:

Servei Emmagatzematge Per què
cataleg PostgreSQL + Redis com a memòria cau + magatzem d'objectes per a fotos Moltes lectures repetides (memòria cau), dades relacionals senzilles, fotos grans fora de la base de dades (símptoma 1)
comandes Cassandra Volum alt d'escriptures, consultes per client i per data, necessitat d'escalar horitzontalment i de disponibilitat molt alta
inventari PostgreSQL replicat Necessita consistència forta (no vendre dues vegades l'última unitat) i transaccions; la replicació dona disponibilitat
pagaments PostgreSQL Transaccions, auditoria, volum moderat
repartiment Cassandra (posicions, sèrie temporal) + PostgreSQL (assignacions) 2,4 milions de posicions al dia; escriptures massives per clau de repartidor i temps (símptoma 4)
analitica Data lake sobre magatzem d'objectes + processament per lots (Spark) i en flux (Flink/Kafka Streams) Consultes massives separades de producció (símptoma 5)

La conseqüència d'aquesta decisió és que la transacció única de confirmar comanda desapareix: reservar estoc (PostgreSQL d'inventari), cobrar (PostgreSQL de pagaments) i crear la comanda (Cassandra) són tres operacions en tres sistemes. Coordinar-les sense transacció global és el problema de les sagues (03-05).

Temps real per als repartidors

Les apps dels repartidors envien posicions per MQTT (protocol lleuger per a xarxes mòbils) i els clients reben actualitzacions per WebSocket, a través d'un servidor de temps real que publica al bus i se n'alimenta (símptoma 4; lliçons 02-01 i 08-02).

Analítica: lots i flux

analitica deixa de consultar producció. Els esdeveniments del bus es desen en un data lake, sobre el qual corren treballs per lots (informes nocturns, amb Spark) i processament en flux (mètriques en temps real de la campanya, amb Flink o Kafka Streams), orquestrats amb un planificador (símptoma 5; Mòdul 5).

Seguretat, observabilitat, desplegament

Una passarel·la autentica els usuaris (OIDC) i limita el trànsit; els serveis s'autentiquen entre si amb mTLS; els secrets es gestionen de manera centralitzada (Mòdul 6). Mètriques, logs i traces distribuïdes es recullen de tots els serveis (Mòdul 7). Tot es desplega com a contenidors a Kubernetes, en un proveïdor de núvol, amb infraestructura definida com a codi (07-05, 08-03).

  1. Mapa de l'arquitectura al curs

Aquesta taula és la brúixola del curs: cada component de l'arquitectura objectiu, i la lliçó en què es construeix o s'explica.

Component / decisió Mòdul Lliçó
Sockets, TCP/UDP, HTTP, MQTT per a repartidors 2 02-01
Crides síncrones entre serveis (RPC) 2 02-02
gRPC i serialització amb contractes (comandesinventari, pagaments) 2 02-03
Bus d'esdeveniments Kafka, cues RabbitMQ 2 02-04
Idempotència, outbox, cues de missatges morts, consumidors en competència 2 02-05
Quines garanties dona cada emmagatzematge (models de consistència) 3 03-01
Per què inventari (consistència) i comandes (disponibilitat) trien diferent: CAP i PACELC 3 03-02
Com es tria el líder de PostgreSQL replicat i com es coordina Kafka: consens (Raft) 3 03-03
Replicació de PostgreSQL d'inventari; detecció de conflictes 3 03-04
Confirmar comanda sense transacció global: sagues 3 03-05
Com Cassandra reparteix les comandes i les posicions: particionament i hashing consistent 4 04-01
Sistemes de fitxers distribuïts per al data lake 4 04-02
Fotos de productes al magatzem d'objectes (S3/MinIO) 4 04-03
Cassandra per a comandes i per a les posicions de repartiment 4 04-04
Redis com a memòria cau del catàleg 4 04-05
Models de computació distribuïda per a analitica 5 05-01
Informes per lots: MapReduce i Hadoop 5 05-02
Previsió de demanda amb Spark 5 05-03
Mètriques de campanya en temps real: Flink / Kafka Streams 5 05-04
Orquestració de treballs nocturns amb Airflow 5 05-05
Autenticació de clients i rols (JWT) 6 06-01
Xifratge de dades de pagament i de posicions 6 06-02
Identitat d'usuaris i productors (OAuth2/OIDC) 6 06-03
mTLS entre serveis, gestió de secrets 6 06-04
Passarel·la, limitació de taxa, auditoria 6 06-05
Mètriques amb Prometheus i Grafana 7 07-01
Logs centralitzats i traces amb OpenTelemetry 7 07-02
Failover de PostgreSQL, checkpoints de Flink 7 07-03
Timeouts, reintents, circuit breaker a comandespagaments 7 07-04
Desplegament a Kubernetes, Ansible 7 07-05
Proves i enginyeria del caos 7 07-06
Microserveis: límits, contractes, organització 8 08-01
WebSockets i MQTT per al seguiment en temps real 8 08-02
Desplegament a AWS/GCP 8 08-03
Funcions serverless per al redimensionament de fotos; edge per als mercats 8 08-04
Quilòmetre Zero d'extrem a extrem 8 08-05

  1. Un camí gradual, no un "big bang"

L'arquitectura objectiu és la destinació, no el primer pas. Reescriure-ho tot de cop és la manera més segura de fracassar: durant mesos no hi hauria res a desplegar, i al final caldria migrar-ho tot alhora. En lloc d'això, Quilòmetre Zero seguirà el patró conegut com a estrangulament (strangler fig): s'extreu un servei del monòlit, s'hi redirigeix el trànsit i el monòlit perd aquesta responsabilitat; es repeteix amb el següent. A cada pas, el sistema funciona i es pot desplegar.

Fase Què s'extreu Símptoma que resol Mòduls del curs
1 cataleg (amb Redis i magatzem de fotos) 1: pics de campanya 2, 4
2 pagaments (aïllat, amb timeouts i circuit breaker) 2: fallada de pagaments que ho tomba tot 2, 7
3 repartiment amb el bus d'esdeveniments i el canal en temps real 4: telemetria 2, 4, 8
4 comandes i inventari (amb sagues i replicació) 3: desplegaments independents 3, 4
5 analitica sobre el bus i el data lake 5: analítica 5
Transversal Seguretat, observabilitat, Kubernetes Tots 6, 7

Es comença pel catàleg perquè és el que causa el símptoma més greu, el que té menys dependències (només lectures) i el que té menys risc: si el servei nou falla, es torna a apuntar al monòlit.

  1. Preparació de l'entorn de treball: el projecte km0/

Per seguir el curs de manera pràctica, crearem un projecte que anirem ampliant. En aquest mòdul només conté el punt de partida: el monòlit i la seva base de dades. No es pretén que el monòlit estigui implementat en detall; n'hi ha prou amb un esquelet que arrenqui, perquè als mòduls següents tinguem on afegir els serveis.

Estructura de carpetes

km0/
├── docker-compose.yml          # Infraestructura local: creixerà mòdul a mòdul
├── README.md
├── monolit/                    # El punt de partida (s'anirà buidant)
│   ├── Dockerfile
│   ├── requirements.txt
│   ├── app.py                  # Punt d'entrada de l'aplicació
│   ├── cataleg/                # Un paquet Python per mòdul de domini
│   ├── comandes/
│   ├── inventari/
│   ├── pagaments/
│   ├── repartiment/
│   └── analitica/
├── serveis/                    # Buit de moment. Mòdul 2: cataleg/, comandes/ ...
├── infra/                      # Buit de moment. Mòdul 7: kubernetes/, ansible/
└── sql/
    └── 001_esquema_inicial.sql # Taules del monòlit

Les carpetes serveis/ i infra/ són buides a propòsit: existeixen perquè l'evolució sigui visible. A mesura que s'extregui cada servei, apareixerà a serveis/ i desapareixerà de monolit/.

docker-compose.yml mínim

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: km0
      POSTGRES_USER: km0
      POSTGRES_PASSWORD: km0_dev        # només per a desenvolupament local
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./sql:/docker-entrypoint-initdb.d   # executa els .sql en crear la BD
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U km0 -d km0"]
      interval: 5s
      timeout: 3s
      retries: 10

  monolit:
    build: ./monolit
    environment:
      DATABASE_URL: postgresql://km0:km0_dev@postgres:5432/km0
    ports:
      - "8000:8000"
    depends_on:
      postgres:
        condition: service_healthy     # espera que PostgreSQL respongui

volumes:
  postgres_data:

Explicació de les parts rellevants per al curs:

  • services declara dos contenidors: postgres i monolit. Docker Compose crea una xarxa interna en què cada servei és accessible pel seu nom: per això la URL de connexió fa servir postgres com a host, i no una IP (recorda la fal·làcia 5: la topologia canvia; els noms són estables, les adreces no).
  • healthcheck i depends_on ... condition: service_healthy són el primer exemple pràctic de "no assumeixis que l'altre node està a punt": el monòlit no arrenca fins que PostgreSQL respon a pg_isready. Sense això, el monòlit arrencaria, intentaria connectar-se, fallaria i moriria. Aquest patró es repetirà amb cada servei que afegim.
  • volumes persisteix les dades de PostgreSQL entre reinicis del contenidor (model de fallada crash-recovery: el procés mor, les dades al disc sobreviuen).
  • ./sql:/docker-entrypoint-initdb.d executa l'esquema inicial la primera vegada que es crea la base de dades.

Esquelet del monòlit

monolit/app.py és deliberadament mínim: comprova que pot parlar amb la base de dades i exposa un punt de salut. Fa servir només la biblioteca estàndard més psycopg per no introduir dependències que es triaran en mòduls posteriors.

import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

import psycopg

DATABASE_URL = os.environ["DATABASE_URL"]


def comptar_productes() -> int:
    """Consulta mínima per comprovar que la base de dades respon."""
    with psycopg.connect(DATABASE_URL) as conn:
        with conn.cursor() as cur:
            cur.execute("SELECT count(*) FROM productes")
            return cur.fetchone()[0]


class Gestor(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/salut":
            cos = {"estat": "ok", "productes": comptar_productes()}
            self._respondre(200, cos)
        else:
            self._respondre(404, {"error": "no trobat"})

    def _respondre(self, codi: int, cos: dict) -> None:
        dades = json.dumps(cos).encode()
        self.send_response(codi)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(dades)))
        self.end_headers()
        self.wfile.write(dades)


if __name__ == "__main__":
    print("Quilòmetre Zero (monòlit) escoltant a :8000")
    HTTPServer(("0.0.0.0", 8000), Gestor).serve_forever()

monolit/requirements.txt conté una única línia, psycopg[binary]>=3.1, i monolit/Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

I sql/001_esquema_inicial.sql amb un esquema mínim i els productors del curs:

CREATE TABLE productors (
    id     SERIAL PRIMARY KEY,
    nom    TEXT NOT NULL,
    ciutat TEXT NOT NULL
);

CREATE TABLE productes (
    id           SERIAL PRIMARY KEY,
    productor_id INTEGER NOT NULL REFERENCES productors(id),
    nom          TEXT NOT NULL,
    preu         NUMERIC(10, 2) NOT NULL,   -- mai float per a diners (fal·làcia 8)
    estoc        INTEGER NOT NULL DEFAULT 0
);

INSERT INTO productors (nom, ciutat) VALUES
    ('Horta La Vega', 'Lleida'),
    ('Formatgeria Montblanc', 'Girona'),
    ('Celler Roure Alt', 'Tarragona');

INSERT INTO productes (productor_id, nom, preu, estoc) VALUES
    (1, 'Tomàquet rosa', 3.20, 120),
    (1, 'Carbassó', 2.10, 80),
    (2, 'Formatge curat d''ovella', 14.50, 5),
    (2, 'Formatge fresc', 6.80, 30),
    (3, 'Vi negre criança', 9.90, 200);

Per arrencar l'entorn:

cd km0
docker compose up --build

I, en una altra terminal, comprovar que el monòlit respon:

curl http://localhost:8000/salut

La resposta ha de ser {"estat": "ok", "productes": 5}.

Amb això queda muntat el punt de partida: exactament el monòlit de la lliçó 01-01, ara executable en contenidors. Al Mòdul 2 apareixerà serveis/cataleg/ i una segona entrada a docker-compose.yml.

Errors Comuns i Consells

  • Partir el monòlit per capes tècniques en lloc de per dominis. Crear un "servei de base de dades", un "servei de lògica" i un "servei d'API" no aïlla res: cada operació continua travessant els tres. Els límits correctes són els de negoci (cataleg, comandes...), que és on l'acoblament és menor.
  • Extreure serveis que comparteixen base de dades. Si comandes i inventari es despleguen per separat però llegeixen i escriuen les mateixes taules, no són dos serveis: són un monòlit distribuït, amb tots els costos de la distribució i cap dels seus avantatges.
  • Fer totes les comunicacions síncrones. Converteix cada flux en una cadena de disponibilitats multiplicades i de latències sumades. La pregunta per a cada interacció ha de ser "necessito la resposta ara?"; si no, és un esdeveniment.
  • Fer totes les comunicacions asíncrones. L'extrem oposat també falla: comprovar l'estoc per esdeveniments, esperant que "eventualment" arribi la resposta, fa impossible donar una confirmació immediata al client. El síncron existeix per una raó.
  • Triar tecnologies abans que problemes. "Farem servir Kafka i Cassandra" no és una decisió d'arquitectura si no respon a un símptoma. Cada peça de l'arquitectura objectiu està justificada per un dels cinc símptomes; si no pots anomenar el símptoma, treu la peça.
  • Deixar l'observabilitat i la seguretat per al final. Quan hi ha sis serveis i alguna cosa falla, ja és tard per preguntar-se com correlacionar logs. Totes dues són part del primer servei que s'extreu.
  • Consell: mantén un document de decisions d'arquitectura (un ADR per decisió: context, opcions, decisió, conseqüències). D'aquí a un any, quan algú pregunti "per què Cassandra per a comandes?", la resposta estarà escrita.

Exercicis

Exercici 1: Identificar límits de serveis

L'equip proposa afegir dues funcionalitats: (a) valoracions i comentaris dels clients sobre els productes, i (b) cupons de descompte per campanya, que s'apliquen a l'import de la comanda i tenen un nombre màxim d'usos. Per a cadascuna, decideix si ha de ser un servei nou o part d'un d'existent, indica de quines dades seria propietària i quines comunicacions (síncrones o asíncrones) necessitaria amb la resta. Justifica cada decisió amb els principis de l'apartat 3.

Exercici 2: Triar el tipus de comunicació

Per a cada interacció, indica si ha de ser síncrona (RPC) o asíncrona (esdeveniment), i raona què passaria amb l'alternativa:

  1. comandes necessita saber si hi ha estoc abans de confirmar una comanda.
  2. cataleg vol mostrar "queden poques unitats" quan l'estoc baixa de 5.
  3. L'app de l'Anna vol mostrar la confirmació del pagament immediatament després de prémer "pagar".
  4. analitica vol comptabilitzar cada venda per a l'informe de campanya.
  5. repartiment necessita conèixer l'adreça de lliurament de cada comanda pagada per planificar rutes.

Exercici 3: Degradació controlada

Dissenya, per a cadascuna de les fallades següents, què ha de continuar funcionant a Quilòmetre Zero i quin missatge o comportament veurà l'usuari. Indica quin principi de disseny ho fa possible:

  1. Cau pagaments (la passarel·la externa no respon).
  2. Cau Redis (la memòria cau del catàleg).
  3. Cau el bus d'esdeveniments (Kafka) durant 10 minuts.
  4. Cau repartiment del tot.

Solucions

Solució 1:

(a) Valoracions: és un bon candidat a servei nou (valoracions). Té dades pròpies (valoració, comentari, client, producte, data), un cicle de vida independent (moderació, respostes del productor), un patró de càrrega diferent (moltes lectures en veure un producte, poques escriptures) i es podria escalar per separat. Comunicació: asíncrona amb comandes (consumeix ComandaLliurada per permetre valorar només productes comprats) i asíncrona cap a cataleg (publica ValoracioCreada perquè el catàleg mantingui la nota mitjana desnormalitzada, sense crides síncrones en mostrar la fitxa). El principi: cada servei propietari de les seves dades; síncron només quan la resposta fa falta ara (mostrar la nota mitjana no necessita consultar valoracions a cada petició).

(b) Cupons: és més discutible. El nombre màxim d'usos exigeix una comprovació amb consistència forta en el moment de confirmar la comanda (no es pot aplicar el cupó 101 si el màxim és 100), cosa que l'acosta al problema de l'estoc. Dues opcions raonables: incloure'l a comandes (que ja gestiona l'import i la confirmació) o crear promocions com a servei propi amb una crida síncrona reservar_us(cupo) des de comandes, anàloga a la reserva d'estoc. Un servei propi es justifica si les campanyes creixeran en complexitat (regles, segmentació, equip de màrqueting amb el seu propi ritme de desplegament); si només són cupons simples, posar-ho a comandes evita una frontera de xarxa al flux més crític. L'important és reconèixer que la resposta depèn del símptoma que es vulgui resoldre, no d'una regla fixa.

Solució 2:

  1. Síncrona. El client espera la confirmació; sense resposta immediata no es pot decidir. Amb esdeveniments, comandes hauria d'esperar "una estona" que inventari respongués per un altre canal, i no podria donar una resposta al client a la mateixa petició.
  2. Asíncrona. cataleg consumeix EstocActualitzat i manté un indicador local. Amb crides síncrones, cada visualització de producte faria una crida a inventari, multiplicant la càrrega i afegint una dependència al flux més massiu (símptoma 1).
  3. Síncrona a la interacció comandespagaments (el client espera), però amb timeout curt i un pla de degradació: si pagaments no respon en 3 segons, la comanda queda en "pagament pendent" i es resol després mitjançant esdeveniments (PagamentConfirmat), avisant la clienta. És una barreja: síncrona al camí feliç, asíncrona com a xarxa de seguretat.
  4. Asíncrona. analitica consumeix ComandaPagada. Una crida síncrona a analitica des de comandes acoblaria el flux de compra a un sistema analític que no necessita estar disponible per vendre (símptoma 5).
  5. Asíncrona. repartiment consumeix ComandaPagada, que inclou l'adreça. Planificar rutes no és immediat; i si repartiment està caigut, l'esdeveniment espera al bus i es processa després.

Solució 3:

  1. Cau pagaments. Catàleg, cistell, seguiment de repartiment i analítica continuen funcionant. En confirmar una comanda, després del timeout, el client veu: "No hem pogut processar el pagament ara mateix; la teva comanda queda desada i t'avisarem tan bon punt es completi" (comanda en estat "pagament pendent"; l'estoc es reserva amb caducitat). Principi: aïllament de fallades + timeouts + asincronia com a xarxa de seguretat. Compara-ho amb el símptoma 2, on aquesta mateixa fallada tombava tota la plataforma.
  2. Cau Redis. cataleg continua funcionant llegint del seu PostgreSQL, més lent i amb menys capacitat; durant una campanya podria ser necessari limitar el trànsit. L'usuari no veu cap error, només més latència. Principi: la memòria cau és una optimització, no una font de veritat; el servei ha de funcionar sense ella.
  3. Cau Kafka 10 minuts. Les operacions síncrones (comprar, pagar, reservar estoc) continuen funcionant. Els esdeveniments s'acumulen a cada servei productor (patró outbox, lliçó 02-05) i es publiquen quan el bus torna. Efectes visibles: el mapa de repartidors es congela, l'indicador "queden poques unitats" es retarda, els informes de campanya van 10 minuts endarrerits. Principi: síncron només quan cal; l'asíncron tolera retards per definició.
  4. Cau repartiment. Es pot comprar i pagar amb normalitat. El seguiment al mapa mostra "Seguiment no disponible temporalment; la teva comanda està confirmada". Les posicions dels repartidors s'acumulen al bus i es processen en recuperar-se. Els esdeveniments ComandaPagada esperen; les rutes es planifiquen amb retard. Principi: aïllament de fallades i desacoblament temporal mitjançant esdeveniments.

Conclusió

Aquesta lliçó ha convertit els fonaments del mòdul en un pla. Hem obert el monòlit de Quilòmetre Zero —sis mòduls, una base de dades, una transacció que abasta fins a la passarel·la de pagaments externa, un desplegament de dimarts i dijous— i hem identificat els cinc símptomes que en forcen l'evolució: els pics de campanya, la fallada de pagaments que ho tomba tot, els equips que es trepitgen en desplegar, la telemetria de repartidors en temps real i l'analítica que ofega la producció. Cada símptoma justifica una peça de l'arquitectura objectiu: sis serveis propietaris de les seves dades, comunicació síncrona per gRPC on cal resposta immediata i asíncrona per esdeveniments a Kafka en tota la resta, emmagatzematge adaptat a cada càrrega (PostgreSQL replicat, Cassandra, Redis, magatzem d'objectes), processament per lots i en flux per a l'analítica, i seguretat, observabilitat i desplegament a Kubernetes com a fonaments transversals. La taula de l'apartat 5 és el mapa que enllaça cada component amb la lliçó que el desenvolupa, i el projecte km0/ amb el seu docker-compose.yml mínim és el punt de partida sobre el qual anirem construint. Res d'això no s'ha implementat encara: és un full de ruta, i el camí serà gradual, extraient un servei cada vegada. El primer pas és fer que dos processos parlin entre si de manera fiable, i això és exactament el que comença al Mòdul 2, Comunicació en Sistemes Distribuïts, amb els protocols de xarxa sobre els quals es recolza tota la resta.

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