Ja sabem quins contextos hi ha a TechCorp i què desa cadascun de "producte" i de "client". Falta la decisió que més costa de prendre i la que té més conseqüències: separar físicament les dades. Mentre els sis serveis continuïn llegint i escrivint l'única base de dades techcorp, no hi haurà autonomia real: qualsevol canvi d'esquema serà una negociació entre equips, qualsevol consulta pesada del catàleg bloquejarà una migració de comandes i el "microservei" serà un monòlit distribuït amb més latència.
Aquesta lliçó desenvolupa el patró database-per-service: per què la base de dades compartida és l'antipatró principal, quins graus d'aïllament existeixen (esquemes separats, instàncies separades, tecnologies diferents), com es trenquen les claus foranes creuades de l'esquema de 01-05, què es fa amb les consultes que avui són un JOIN, qui és propietari de cada dada, com es migren les dades sense aturar la botiga i, com a resultat, quin esquema concret tindrà cada servei de TechCorp (SQL de servei-comandes i servei-inventari, document del catàleg a MongoDB). Tanquem avançant la qüestió del reporting. La consistència entre aquestes bases de dades separades (sagues, CQRS, event sourcing) és el tema de la lliçó següent.
Contingut
- El patró database-per-service i l'antipatró de la base de dades compartida
- Graus d'aïllament: de l'esquema separat a la persistència políglota
- Trencar les claus foranes creuades
- Les consultes que abans eren un
JOIN - Propietat i cicle de vida de les dades
- Migrar les dades sense aturar la botiga
- L'esquema resultant de cada servei de TechCorp
- Implicacions per al reporting i l'analítica
- El patró database-per-service i l'antipatró de la base de dades compartida
El patró s'enuncia en una línia: cada servei és l'únic que accedeix al seu emmagatzematge; els altres només arriben a aquestes dades a través de la seva API o dels seus esdeveniments. "Emmagatzematge" inclou taules, col·leccions, índexs, cues privades i fitxers. És la materialització del principi "dades descentralitzades" de 02-01 i del "contracte com a única porta".
Per què la base de dades compartida és l'antipatró principal (i no un més):
| Amb base de dades compartida | Conseqüència | Problema de TechCorp que reprodueix |
|---|---|---|
| Qualsevol servei pot llegir qualsevol taula. | L'esquema de tots és contracte de tots: ningú no pot canviar una columna sense auditar els altres. | Notificacions va trencar un informe de Comandes en canviar una consulta. |
| Qualsevol servei pot escriure qualsevol taula. | Els invariants d'un context els "protegeix" codi repartit en diversos serveis. | crearComanda escriu a estoc i pagaments. |
| Una migració d'esquema bloqueja tothom. | El desplegament d'un servei depèn de l'estat de la BD dels altres. | La migració de linies_comanda va fallar per una consulta de Catàleg. |
| Un servei amb consultes pesades degrada la BD de tots. | No hi ha aïllament de rendiment. | Black Friday: el catàleg va tombar els cobraments. |
| Una única tecnologia per a tothom. | El model de dades es força perquè càpiga a la BD triada. | atributs_producte clau-valor a PostgreSQL. |
En resum: amb la base de dades compartida es compleixen tots els acoblaments de 02-01 (de dades, de desplegament, temporal) alhora. És per això que la lliçó 01-01 ho va definir com "el que un microservei no és".
El que es paga a canvi, i que la resta de la lliçó gestiona: es perden els JOIN entre contextos, es perden les claus foranes creuades i es perd la transacció única.
- Graus d'aïllament: de l'esquema separat a la persistència políglota
Separar les dades no és tot o res. Hi ha una escala, i pujar-la graó a graó és una estratègia de migració legítima:
| Graó | Què se separa | Què aïlla | Què no aïlla | Ús a TechCorp |
|---|---|---|---|---|
| 0. Taules privades per convenció | Res de físic; només la regla "no toquis taules alienes". | L'acoblament d'escriptura, si tothom la respecta. | Res tècnicament: un JOIN continua sent possible. |
Primer pas dins del monòlit (02-02, refactorització prèvia). |
| 1. Esquemes separats a la mateixa instància | Un SCHEMA de PostgreSQL per servei (comandes.*, inventari.*), amb usuaris diferents i permisos que impedeixen llegir esquemes aliens. |
Acoblament de dades i de desplegament d'esquema (cada servei migra el seu). | Rendiment (mateixa CPU, disc i connexions), disponibilitat (mateixa instància), còpies de seguretat. | Pas intermedi per a clients, inventari, comandes i pagaments durant la migració. |
| 2. Instàncies separades | Un servidor (o clúster) de base de dades per servei. | Tot l'anterior més rendiment i disponibilitat. | Res de rellevant; cost operatiu més alt. | Objectiu per a comandes i inventari tan bon punt la migració avanci; a Kubernetes, un StatefulSet o servei gestionat per BD (05-02). |
| 3. Tecnologies diferents (polyglot persistence) | Cada servei tria el tipus de magatzem més adequat. | A més, el model de dades deixa de forçar-se. | Augmenta la varietat operativa: més coses que cal saber operar. | Catàleg a MongoDB; la resta a PostgreSQL. |
flowchart LR
subgraph AVUI[Avui: una instància, un esquema]
M[(techcorp<br/>public.*)]
end
subgraph PAS1[Pas intermedi: una instància, esquemes i usuaris separats]
P1[(pg-techcorp)]
P1 --- S1[schema comandes<br/>usuari svc_comandes]
P1 --- S2[schema inventari<br/>usuari svc_inventari]
P1 --- S3[schema clients<br/>usuari svc_clients]
P1 --- S4[schema pagaments<br/>usuari svc_pagaments]
end
subgraph OBJ[Objectiu: instàncies i tecnologies per servei]
O1[(PG comandes)]
O2[(PG inventari)]
O3[(PG clients)]
O4[(PG pagaments)]
O5[(MongoDB catàleg)]
end
AVUI --> PAS1 --> OBJ
El graó 1 és barat i ja elimina el problema més greu (l'acoblament d'esquema). Com es fa a PostgreSQL:
-- A la instància actual: un esquema i un usuari per servei.
CREATE SCHEMA comandes;
CREATE ROLE svc_comandes LOGIN PASSWORD '...';
GRANT USAGE, CREATE ON SCHEMA comandes TO svc_comandes;
-- I, sobretot: l'usuari de comandes NO té permisos sobre els altres esquemes.
REVOKE ALL ON SCHEMA public FROM svc_comandes;
-- Repetir per a inventari, clients i pagaments.Amb això, la línia SELECT ... FROM clients de crearComanda falla en temps d'execució si el servei de comandes fa servir svc_comandes. És una manera eficaç que la regla "no toquis taules alienes" deixi de ser una convenció i passi a ser una restricció tècnica.
2.1 Per què el catàleg va a MongoDB i la resta a PostgreSQL
La persistència políglota no és "fer servir tecnologies variades perquè es pot"; és triar pel model de dades i el patró d'accés:
| Criteri | Catàleg | Comandes, Inventari, Pagaments, Clients |
|---|---|---|
| Forma de les dades | Documents amb atributs variables per categoria (voltatge, talla, compatibilitat): avui, la incòmoda taula clau-valor atributs_producte. |
Files regulars amb relacions internes clares (comanda-línies, estoc-reserves). |
| Patró d'accés | Lectura massiva per fitxa i cerca amb filtres per atributs; escriptura poc freqüent. | Escriptures transaccionals amb invariants (quantitat - reservat >= 0, total de la comanda, no cobrar dues vegades). |
| Necessitat de transaccions | Baixa: publicar una fitxa és una escriptura d'un document. | Alta: la reserva de diverses línies d'una comanda ha de ser atòmica dins del servei. |
| Escalat | Horitzontal de lectura (rèpliques, índexs sobre atributs, fins i tot cerca de text). | Vertical o amb rèpliques de lectura; volum manejable (3.000 comandes/dia). |
| Decisió | MongoDB: un document per producte amb atributs com a subdocument; índexs per categoria i per atributs freqüents. |
PostgreSQL: transaccions ACID dins de cada servei, restriccions, tipus numèrics exactes per a diners. |
I una regla de prudència que la Marta imposa: com a màxim dues tecnologies de base de dades al sistema. Cada tecnologia nova és còpia de seguretat, monitoratge, coneixement i guàrdies diferents. MongoDB es justifica pel problema 5 de 01-05; una tercera tecnologia necessitaria un problema igual de concret.
- Trencar les claus foranes creuades
Recuperem l'esquema de 01-05 i assignem propietari a cada taula, marcant les claus foranes que creuen fronteres:
| Taula | Propietari | Clau forana actual | Creua frontera? | Què hi passa |
|---|---|---|---|---|
clients |
Clients | — | — | — |
productes, atributs_producte |
Catàleg | atributs_producte.producte_id → productes |
No (interna) | Desapareix: passen a ser un document. |
estoc |
Inventari | estoc.producte_id → productes |
Sí | S'elimina la FK; producte_id queda com a referència opaca. |
comandes |
Comandes | comandes.client_id → clients |
Sí | S'elimina la FK; client_id queda com a referència opaca. |
linies_comanda |
Comandes | linies_comanda.comanda_id → comandes |
No (interna) | Es manté: és la relació arrel-línia de l'agregat. |
linies_comanda |
Comandes | linies_comanda.producte_id → productes |
Sí | S'elimina la FK; s'afegeixen columnes copiades (nom_producte, preu_unitari). |
pagaments |
Pagaments | pagaments.comanda_id → comandes |
Sí | S'elimina la FK; comanda_id queda com a referència opaca. |
notificacions |
Notificacions | client_id (ja avui sense FK) |
— | Igual. |
Una clau forana creuada és incompatible amb database-per-service per definició: PostgreSQL no pot comprovar que comandes.client_id existeix en una taula que és en una altra base de dades (o en un altre servidor). Així que se substitueix per una referència per identificador: una columna amb l'id de l'altre context, sense restricció d'integritat a la base de dades.
Què es perd i com es compensa:
| Garantia de la FK | Com es recupera sense FK |
|---|---|
| No es pot inserir una comanda d'un client inexistent. | El servei valida per contracte en crear: servei-comandes consulta GET /clients/{id} (o comprova la seva rèplica local) abans d'acceptar la comanda. És el que fa avui el pas 1 de crearComanda, però per API. |
| No es pot esborrar un client amb comandes. | Se substitueix per política de negoci: els clients no s'esborren, es donen de baixa (esborrat lògic) o s'anonimitzen; i servei-clients publica client.eliminat perquè els altres reaccionin (apartat 5). |
JOIN barat per obtenir el nom del client. |
Es perd de debò; vegeu l'apartat 4. |
| Índex implícit i coherència de tipus. | Es creen explícitament: CREATE INDEX sobre client_id; els identificadors passen a ser cadenes opaques (c-1024, p-501, com-88213), no SERIAL que només tenen sentit dins d'una base de dades. |
L'últim punt mereix èmfasi: tan bon punt un identificador viatja entre serveis, deixa de poder ser un enter autoincremental local. servei-comandes no pot generar comanda_id = 42 i servei-pagaments emmagatzemar 42, perquè si un dia es restaura una còpia o es particiona la taula, el 42 deixa de ser únic. TechCorp adopta identificadors de text generats pel servei propietari (al curs, amb prefix llegible: com-, c-, p-, pag-, res-; en producció, UUID o ULID amb aquest prefix).
- Les consultes que abans eren un
JOIN
JOINEl cas paradigmàtic de TechCorp: el tauler d'administració mostra "comandes del dia amb nom del client i nom dels productes". Avui:
-- Monòlit: un JOIN entre tres àrees, trivial amb una sola BD
SELECT p.id, c.nom AS client, pr.nom AS producte, l.quantitat, p.total
FROM comandes p
JOIN clients c ON c.id = p.client_id
JOIN linies_comanda l ON l.comanda_id = p.id
JOIN productes pr ON pr.id = l.producte_id
WHERE p.creat_en::date = CURRENT_DATE;Amb clients en una base de dades, productes a MongoDB i comandes en una altra, aquest JOIN no existeix. Hi ha tres alternatives, i triar bé depèn de la freqüència, la tolerància a dades lleugerament desactualitzades i de qui és la dada:
| Alternativa | Com funciona | Avantatges | Inconvenients | Quan fer-la servir |
|---|---|---|---|---|
| Composició d'APIs (API composition) | Qui necessita la dada composta consulta cada servei i uneix els resultats en memòria. | Sense duplicar dades; sempre actualitzat. | Acoblament temporal (N crides); latència; problema de l'"N+1"; impossible paginar/filtrar per camps aliens. | Consultes poc freqüents o de pocs elements (una pantalla de detall). |
| Rèplica de dades via esdeveniments | El servei consumidor manté una còpia local de només lectura dels camps que necessita, actualitzada en rebre esdeveniments del propietari (client.actualitzat, producte.actualitzat). |
Consultes locals ràpides, sense dependència en temps d'execució; es pot filtrar i paginar. | Consistència eventual (segons de retard); cal gestionar la subscripció i la càrrega inicial. | Dades que es llegeixen molt, canvien poc i toleren retard: nom de client, nom de producte. |
| Vista materialitzada / model de lectura (CQRS) | Un component construeix, a partir dels esdeveniments de diversos serveis, una taula o índex pensat exclusivament per a aquesta consulta. | Consultes complexes i ràpides; no carrega els serveis d'origen. | Una altra peça que cal mantenir; consistència eventual. | Taulers, llistats amb filtres creuats, cerques. Es desenvolupa a 02-05. |
En codi, la composició d'APIs s'assembla a això (esquelet; el client HTTP real, amb timeouts i reintents, es construeix a 04-04):
// Composició d'APIs per a "detall d'una comanda amb nom de client"
// S'executa a qui necessita la vista composta (per exemple, un BFF del tauler d'administració, 03-04).
async function detallComandaAmbClient(comandaId) {
const comanda = await comandesApi.obtenir(comandaId); // GET /comandes/com-88213
const client = await clientsApi.obtenir(comanda.clientId); // GET /clients/c-1024
return {
...comanda,
client: { nom: client.nom, email: client.email } // només el que la vista necessita
};
}
// Per a un llistat de 200 comandes, això són 200 crides a Clients: l'"N+1".
// Aquí convé la rèplica o la vista materialitzada.I la rèplica local, a l'esquema de servei-comandes:
-- Rèplica de només lectura, dins de la BD de comandes, mantinguda per esdeveniments de Clients.
-- Només els camps que Comandes necessita per als seus llistats. Mai no s'escriu des de l'API de Comandes.
CREATE TABLE clients_ref (
client_id TEXT PRIMARY KEY,
nom TEXT NOT NULL,
email TEXT NOT NULL,
actualitzat_en TIMESTAMPTZ NOT NULL -- data de l'esdeveniment que la va actualitzar
);Decisió de TechCorp per al flux de comanda:
- En crear una comanda,
servei-comandesconsulta Catàleg (preu i nom actuals) i Clients (existència, email, adreça) per composició: són dues crides, la dada ha de ser l'actual i la comanda no es pot crear sense elles. És l'acoblament temporal deliberat que vam assenyalar a 02-02. - Per llistar comandes amb nom de client,
servei-comandesfa servir la seva rèplicaclients_ref. - Per al tauler d'administració amb filtres creuats, una vista materialitzada alimentada per esdeveniments (02-05).
Fixa't que les còpies congelades de linies_comanda (nom_producte, preu_unitari) no són una rèplica: no s'actualitzen quan el catàleg canvia. Són dades de la comanda. clients_ref sí que és una rèplica: si el client canvia de nom, ho ha de reflectir.
- Propietat i cicle de vida de les dades
Amb les dades separades cal respondre, dada per dada, tres preguntes: qui escriu, qui llegeix i qui decideix quan desapareix.
| Dada | Escriu (únic) | Llegeix | Com la llegeixen els altres | Cicle de vida |
|---|---|---|---|---|
| Fitxa de producte | Catàleg | Tots | GET /productes, esdeveniment producte.actualitzat |
El despublica Catàleg; mai no s'esborra si hi ha comandes que el referencien (les comandes en desen la seva còpia). |
| Estoc i reserves | Inventari | Comandes (indirectament) | Esdeveniments estoc.reservat / estoc.alliberat; GET /estoc/{producteId} per a la web |
Les reserves expiren o es consumeixen; ho decideix Inventari. |
| Comanda i línies | Comandes | Pagaments, Notificacions, tauler | Esdeveniments comanda.*; GET /comandes/{id} |
Retenció legal (anys); ho decideix Comandes. |
| Pagament | Pagaments | Comandes | Esdeveniment pagament.confirmat; GET /pagaments?comandaId= |
Retenció legal; les dades de targeta mai no s'emmagatzemen (les té la passarel·la). |
| Client | Clients | Comandes, Notificacions | GET /clients/{id}; esdeveniments client.actualitzat / client.eliminat |
Baixa o anonimització a petició (RGPD). |
| Còpia congelada nom/preu a les línies | Comandes | Comandes | — | Viu i mor amb la comanda. |
Rèplica clients_ref |
Comandes, només des del consumidor d'esdeveniments | Comandes | — | S'actualitza o s'esborra en rebre esdeveniments de Clients. |
Dos casos que la taula resol i que al monòlit ni es plantejaven:
- La rèplica té dos "escriptors" aparents: l'API de Comandes i el consumidor d'esdeveniments de Comandes. Només el segon escriu
clients_ref; convé que l'usuari de base de dades de l'API no tingui permís d'escriptura sobre aquesta taula, o que el codi ho faci impossible per construcció. - El dret a l'oblit (RGPD): quan un client demana ser eliminat, avui n'hi ha prou amb un
DELETE(que fallaria per les FK decomandes). Demà,servei-clientsanonimitza la seva fila i publicaclient.eliminat;servei-comandesesborra la fila declients_refi anonimitza l'adreça d'enviament de les comandes antigues que la llei l'obligui a conservar;servei-notificacionsdeixa de tenir res a esborrar perquè mai no va emmagatzemar el client. L'absència de FK obliga a dissenyar aquest flux explícitament, cosa que és un avantatge: al monòlit estava implícit i mal resolt.
- Migrar les dades sense aturar la botiga
Separar l'esquema és una cosa; moure les dades que ja existeixen (milers de comandes, tot el catàleg) mentre la botiga ven n'és una altra. El procediment estàndard, aplicat a l'extracció del catàleg:
flowchart TB
A[1. Crear el nou magatzem<br/>MongoDB catàleg, buit] --> B[2. Càrrega inicial<br/>script: productes + atributs_producte → documents]
B --> C[3. Sincronització contínua<br/>canvis al monòlit → esdeveniments → MongoDB]
C --> D[4. Lectures al nou magatzem<br/>feature flag CATALEG_REMOT=true, per percentatge]
D --> E{Dades iguals?<br/>comparació en ombra}
E -- no --> C
E -- sí --> F[5. Escriptures al nou magatzem<br/>el monòlit deixa d'escriure productes]
F --> G[6. Retirar la taula antiga<br/>després d'un període de gràcia]
Els punts delicats:
- Pas 3, sincronització. Mentre convisquin les dues còpies, l'antiga continua rebent escriptures (l'equip de catàleg publica fitxes cada dia). Hi ha dues maneres de propagar-les: que el monòlit publiqui un esdeveniment per cada canvi (
producte.actualitzat) que el servei nou consumeix, o captura de canvis (CDC) llegint el registre de transaccions de PostgreSQL amb eines com Debezium. TechCorp tria esdeveniments publicats des del monòlit, perquè aquests mateixos esdeveniments faran falta després. - El risc del dual write. La temptació és que el monòlit escrigui directament a les dues bases de dades ("actualitzo la taula i el document a la mateixa funció"). És un error clàssic: no hi ha cap transacció que abasti PostgreSQL i MongoDB, així que davant de qualsevol fallada entre les dues escriptures (caiguda, timeout, excepció) les còpies divergeixen sense que ningú se n'assabenti. L'escriptura ha de ser una de sola (a la font de veritat) i la propagació, asíncrona i reintentable (esdeveniment o outbox, que veurem a 02-05).
- Pas 4, comparació en ombra. Abans de refiar-se del nou magatzem, durant un temps s'executen les lectures contra tots dos i es comparen els resultats en un registre. Les discrepàncies revelen errors de l'script de càrrega o esdeveniments perduts.
- La feature flag.
CATALEG_REMOTés la commutació del branch by abstraction de 02-02: permet passar l'1 %, el 10 %, el 100 % de les lectures al servei nou i tornar enrere en segons. - Pas 6, no tenir pressa. La taula antiga es conserva en només lectura unes setmanes. Esborrar-la el mateix dia del tall és la manera més ràpida de descobrir que un informe mensual la feia servir.
Un fragment de l'script de càrrega inicial, il·lustratiu:
// scripts/migrar-cataleg.js (s'executa un cop; il·lustratiu)
// Llegeix productes + atributs_producte del monòlit i crea un document per producte.
async function migrarCataleg(pg, mongo) {
const { rows: productes } = await pg.query('SELECT id, nom, preu, categoria, actiu FROM productes');
for (const p of productes) {
const { rows: atributs } = await pg.query(
'SELECT clau, valor FROM atributs_producte WHERE producte_id = $1', [p.id]);
await mongo.collection('productes').updateOne(
{ producteId: `p-${p.id}` }, // id opac amb prefix
{ $set: {
producteId: `p-${p.id}`, nom: p.nom, preu: Number(p.preu),
categoria: p.categoria, publicat: p.actiu,
atributs: Object.fromEntries(atributs.map(a => [a.clau, a.valor])), // clau-valor -> subdocument
migratEn: new Date()
} },
{ upsert: true }); // idempotent: es pot rellançar
}
}Observa upsert: true: l'script es pot executar diverses vegades sense duplicar res. És la primera aparició pràctica de la idempotència que la lliçó següent converteix en regla.
- L'esquema resultant de cada servei de TechCorp
Amb tot l'anterior, així queden els magatzems. Només mostrem complets els dos que més protagonisme tindran a la saga; la resta, resumida.
7.1 servei-comandes (PostgreSQL, base de dades comandes)
-- Base de dades: comandes. Usuari: svc_comandes. Ningú més no s'hi connecta.
CREATE TABLE comandes (
comanda_id TEXT PRIMARY KEY, -- 'com-88213', generat per aquest servei
client_id TEXT NOT NULL, -- referència opaca a Clients: SENSE clau forana
estat TEXT NOT NULL CHECK (estat IN ('PENDENT','ESTOC_RESERVAT','PAGADA','CONFIRMADA','CANCELLADA')),
total NUMERIC(10,2) NOT NULL,
adreca_enviament JSONB NOT NULL, -- còpia congelada de l'adreça en el moment de la comanda
motiu_cancellacio TEXT, -- 'SENSE_ESTOC', 'PAGAMENT_REBUTJAT', ... (s'omple a 02-05)
creat_en TIMESTAMPTZ NOT NULL DEFAULT NOW(),
actualitzat_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_comandes_client ON comandes (client_id); -- explícit: ja no el dona la FK
CREATE TABLE linies_comanda (
comanda_id TEXT NOT NULL REFERENCES comandes(comanda_id), -- FK INTERNA: arrel -> línia de l'agregat
linia INT NOT NULL,
producte_id TEXT NOT NULL, -- referència opaca a Catàleg: SENSE clau forana
nom_producte TEXT NOT NULL, -- còpia congelada
preu_unitari NUMERIC(10,2) NOT NULL, -- còpia congelada
quantitat INT NOT NULL CHECK (quantitat > 0),
PRIMARY KEY (comanda_id, linia)
);
-- Rèplica de només lectura de Clients (apartat 4), alimentada per esdeveniments
CREATE TABLE clients_ref (
client_id TEXT PRIMARY KEY,
nom TEXT NOT NULL,
email TEXT NOT NULL,
actualitzat_en TIMESTAMPTZ NOT NULL
);Compara-ho amb el comandes / linies_comanda de 01-05: han desaparegut les FK a clients i productes, han aparegut les còpies congelades i l'adreça, els identificadors són text, i hi ha dos estats nous (ESTOC_RESERVAT, PAGADA) que la saga de 02-05 explicarà. A 02-05 s'afegirà una taula més a aquesta base de dades: outbox.
7.2 servei-inventari (PostgreSQL, base de dades inventari)
-- Base de dades: inventari. Usuari: svc_inventari.
CREATE TABLE estoc (
producte_id TEXT PRIMARY KEY, -- referència opaca a Catàleg: SENSE clau forana
quantitat INT NOT NULL CHECK (quantitat >= 0),
reservat INT NOT NULL DEFAULT 0 CHECK (reservat >= 0),
ubicacio TEXT,
llindar_reposicio INT NOT NULL DEFAULT 0,
CHECK (reservat <= quantitat) -- l'invariant del context, protegit per la BD
);
CREATE TABLE reserves (
reserva_id TEXT PRIMARY KEY, -- 'res-40021'
comanda_id TEXT NOT NULL UNIQUE, -- una reserva per comanda: base de la idempotència (02-05)
estat TEXT NOT NULL CHECK (estat IN ('ACTIVA','CONSUMIDA','ALLIBERADA')),
creada_en TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expira_en TIMESTAMPTZ -- les reserves no confirmades caduquen
);
CREATE TABLE linies_reserva (
reserva_id TEXT NOT NULL REFERENCES reserves(reserva_id), -- FK interna
producte_id TEXT NOT NULL,
quantitat INT NOT NULL CHECK (quantitat > 0),
PRIMARY KEY (reserva_id, producte_id)
);L'important: l'invariant reservat <= quantitat viu aquí i només aquí, com a restricció de base de dades. Al monòlit, el protegia la clàusula WHERE quantitat - reservat >= $1 escrita a crearComanda, és a dir, al codi d'un altre context.
7.3 servei-cataleg (MongoDB, base de dades cataleg, col·lecció productes)
{
"_id": "p-501",
"producteId": "p-501",
"nom": "Auriculars BT X200",
"descripcio": "Auriculars sense fil amb cancel·lació de soroll...",
"categoria": "audio",
"preu": 59.90,
"publicat": true,
"atributs": { "color": "negre", "autonomiaHores": 30, "bluetooth": "5.3", "cancellacioSoroll": true },
"imatges": ["x200-frontal.webp", "x200-lateral.webp"],
"actualitzatEn": "2026-08-14T09:12:00Z"
}La taula clau-valor atributs_producte (problema 5 de 01-05) desapareix: cada producte porta els atributs que li corresponen, i un índex sobre categoria més índexs selectius sobre atributs freqüents (atributs.color) resolen la cerca amb filtres. No hi ha cap referència a estoc ni a comandes.
7.4 La resta, en una línia cadascun
servei-pagaments(PostgreSQLpagaments): taulapagaments (pagament_id, comanda_id sense FK, import, estat AUTORITZAT/CAPTURAT/REBUTJAT/REEMBORSAT, referencia_passarella, creat_en), ambUNIQUE (comanda_id)per no cobrar dues vegades.servei-clients(PostgreSQLclients):clients (client_id, keycloak_sub, email UNIQUE, nom, actiu)iadreces (adreca_id, client_id FK interna, carrer, cp, ciutat, predeterminada).servei-notificacions: registre mínimenviaments (enviament_id, comanda_id, tipus, destinatari, enviat_en, estat)per no reenviar; pot ser una taula petita o fins i tot el mateix registre de la cua.
- Implicacions per al reporting i l'analítica
Queda una pregunta que la Marta farà tan bon punt vegi el disseny: "i l'informe de vendes per categoria i ciutat?". Avui és un JOIN de quatre taules. Demà les dades són en cinc bases de dades, una d'elles MongoDB.
La resposta de l'arquitectura és que l'analítica no es fa contra les bases de dades operatives dels serveis, ni abans ni després dels microserveis (fer-ho era, de fet, una de les causes dels bloquejos del problema 3). Es construeix una base de dades de lectura separada (magatzem analític o data warehouse) alimentada pels esdeveniments que ja publiquen els serveis (comanda.confirmada amb línies i adreça, producte.actualitzat amb categoria) o per exportacions periòdiques. És la mateixa idea de la vista materialitzada de l'apartat 4, portada a escala d'empresa: consistència eventual (minuts o hores de retard són acceptables per a un informe) a canvi de no tocar els serveis en producció. Se'n desenvolupa la mecànica a 02-05 (CQRS) i no hi tornarem llevat de passada.
Errors Comuns i Consells
- Separar els serveis i deixar la base de dades compartida "de moment". Aquest "moment" dura anys. Com a mínim, esquemes i usuaris separats des del primer servei extret.
- Mantenir les FK creuades "perquè són gratis". No ho són: són la raó per la qual el catàleg no pot traslladar-se a MongoDB ni un servei migrar el seu esquema sense coordinar-se.
- Replicar tot el model de l'altre servei. La rèplica porta els camps que necessites, no la taula sencera.
clients_refté tres columnes, no dotze. - Confondre còpia congelada amb rèplica. El preu d'una línia de comanda no s'ha d'actualitzar; el nom del client a
clients_refsí. Documenta quin és quin. - Fer dual write durant la migració. Una escriptura, a la font de veritat; la còpia es propaga per esdeveniments i es verifica en ombra.
- Fer servir
SERIALcom a identificador que viatja entre serveis. Identificadors opacs, generats pel propietari, únics globalment. - Consell: per a cada consulta del monòlit que avui fa
JOINentre àrees, apunta en una taula: freqüència, tolerància al retard, camps aliens que necessita, i decideix composició / rèplica / vista materialitzada. Aquesta taula és el teu pla de dades.
Exercicis
Exercici 1: Classificar consultes
Per a cada consulta del monòlit, indica l'alternativa més adequada (composició d'APIs, rèplica per esdeveniments o vista materialitzada) i justifica-ho en una línia: (1) la pàgina de detall d'una comanda per al client, que mostra el nom del client i les línies; (2) el llistat del tauler d'administració "comandes d'avui" amb nom de client, paginat i filtrable per ciutat; (3) el correu de confirmació, que necessita l'email del client i els noms dels productes; (4) l'informe mensual de vendes per categoria de producte.
Exercici 2: La FK que falta
pagaments.comanda_id perd la seva clau forana a comandes. Explica quines garanties es perden i com les recupera servei-pagaments per disseny (pensa en: com sap que la comanda existeix, què impedeix dos pagaments per a la mateixa comanda, i què passa si arriba un estoc.reservat d'una comanda que Pagaments no ha vist mai).
Exercici 3: Un esquema per a Clients
Escriu l'SQL de la base de dades clients de servei-clients (taules clients i adreces) seguint les convencions d'aquesta lliçó (identificadors opacs, sense FK creuades, restriccions internes explícites), i indica quin esdeveniment hauria de publicar el servei quan un client canvia el seu email i quins serveis el consumirien.
Solucions
Exercici 1
- Composició (o fins i tot res: la comanda ja desa les línies amb nom i preu congelats; per al nom del client, una crida a
GET /clients/{id}o la rèplicaclients_ref). És una consulta d'un sol element i ha de mostrar l'estat actual. - Rèplica (
clients_ref) per al nom, i probablement vista materialitzada si es filtra per ciutat de l'adreça d'enviament i es combina amb dades d'altres contextos: la paginació i els filtres creuats no funcionen amb composició (N+1). - Cap de les tres a Notificacions: les dades viatgen a l'esdeveniment
comanda.confirmada(email, nom, línies amb nom de producte). Notificacions no consulta ni replica; és conformist (02-03). - Vista materialitzada / magatzem analític alimentat per esdeveniments: consulta agregada, tolerant al retard, que no ha de carregar els serveis en producció (apartat 8).
Exercici 2
Es perd: (a) la garantia que la comanda existeix; (b) el bloqueig d'esborrat; (c) el JOIN. Com ho recupera Pagaments: (a) Pagaments no crea pagaments per iniciativa pròpia: reacciona a estoc.reservat, que porta comandaId i import, i aquest esdeveniment només existeix si Comandes va crear la comanda; opcionalment valida amb GET /comandes/{id} en cas de dubte. (b) La restricció UNIQUE (comanda_id) a la seva pròpia taula pagaments impedeix dos cobraments per a la mateixa comanda encara que l'esdeveniment arribi duplicat (idempotència, 02-05). (c) Si arriba un estoc.reservat d'una comanda desconeguda per a Pagaments, és la situació normal: Pagaments no necessita "haver vist" la comanda abans; l'esdeveniment és l'única entrada. El que sí que ha de fer és rebutjar esdeveniments mal formats o d'import invàlid i registrar el cas. I si una comanda s'anul·la, no s'esborra: Pagaments rebrà comanda.cancellada i reemborsarà si havia cobrat.
Exercici 3
CREATE TABLE clients (
client_id TEXT PRIMARY KEY, -- 'c-1024', generat per aquest servei
keycloak_sub TEXT NOT NULL UNIQUE, -- referència opaca a l'usuari a Keycloak (extern)
email TEXT NOT NULL UNIQUE, -- invariant del context: email únic
nom TEXT NOT NULL,
actiu BOOLEAN NOT NULL DEFAULT TRUE, -- baixa lògica en lloc de DELETE
creat_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE adreces (
adreca_id TEXT PRIMARY KEY,
client_id TEXT NOT NULL REFERENCES clients(client_id), -- FK interna a l'agregat Client
carrer TEXT NOT NULL, cp TEXT NOT NULL, ciutat TEXT NOT NULL,
predeterminada BOOLEAN NOT NULL DEFAULT FALSE
);Esdeveniment: client.actualitzat amb { clientId, email, nom, actualitzatEn }. El consumiria servei-comandes per actualitzar clients_ref (rèplica). servei-notificacions no el necessita (rep l'email a cada esdeveniment de comanda) i servei-pagaments tampoc (no coneix clients). Un client.eliminat addicional dispararia l'anonimització descrita a l'apartat 5.
Conclusió
Hem fet el pas físic que separa de debò els serveis: una base de dades per servei. Hem vist per què la base de dades compartida concentra tots els acoblaments, l'escala d'aïllament (esquemes i usuaris separats com a pas intermedi, instàncies separades com a objectiu, MongoDB per al catàleg i PostgreSQL per a la resta pel seu model de dades i el seu patró d'accés, amb el límit de dues tecnologies), com es trenquen les claus foranes creuades (comandes.client_id, linies_comanda.producte_id, estoc.producte_id, pagaments.comanda_id passen a referències opaques de text, amb validació per contracte i identificadors generats pel propietari), què fer amb els JOIN perduts (composició d'APIs per al detall, rèplica clients_ref alimentada per esdeveniments per als llistats, vista materialitzada per al tauler), qui escriu cada dada i com se'n gestiona el cicle de vida (inclòs l'esborrat RGPD), com migrar sense dual write (càrrega inicial idempotent, sincronització per esdeveniments, lectura en ombra, feature flag) i l'esquema resultant de servei-comandes, servei-inventari i servei-cataleg.
Aquest esquema deixa una pregunta oberta a propòsit: comandes.estat admet ESTOC_RESERVAT i PAGADA, i reserves té expira_en. Són les empremtes del que ja no existeix: la transacció única de crearComanda. La lliçó següent la substitueix per una saga, amb les seves compensacions, la seva màquina d'estats, el patró outbox que garanteix que els esdeveniments surten si i només si el canvi s'ha desat, la idempotència dels consumidors, i les vistes de lectura de CQRS; i decidirà si TechCorp necessita event sourcing (avançament: de moment, no).
Curs de Microserveis
Mòdul 1: Introducció als Microserveis
- Conceptes Bàsics de Microserveis
- Avantatges i Desavantatges dels Microserveis
- Comparació amb l'Arquitectura Monolítica
- Quan Adoptar Microserveis: Criteris de Decisió
- El Cas Pràctic del Curs: la Botiga Online de TechCorp
Mòdul 2: Disseny de Microserveis
- Principis de Disseny de Microserveis
- Descomposició d'Aplicacions Monolítiques
- Definició de Bounded Contexts
- Gestió de Dades: una Base de Dades per Servei
- Consistència Distribuïda: Sagues, CQRS i Event Sourcing
Mòdul 3: Comunicació entre Microserveis
- APIs RESTful
- Missatgeria Asíncrona
- Protocols de Comunicació: gRPC, GraphQL
- API Gateway i Backend for Frontend
- Descobriment de Serveis i Balanceig de Càrrega
- Contractes i Versionat d'APIs
Mòdul 4: Implementació de Microserveis
- Elecció de Tecnologies i Eines
- Desenvolupament d'un Microservei Simple
- Gestió de Configuració
- Integració Pràctica: Consumir APIs i Publicar Esdeveniments
- Proves en Microserveis: Unitàries, d'Integració i de Contracte
Mòdul 5: Desplegament i Orquestració
- Contenidors i Docker
- Orquestració amb Kubernetes
- CI/CD per a Microserveis
- Estratègies de Desplegament: Rolling, Blue-Green i Canary
- Service Mesh: Istio i Linkerd
Mòdul 6: Monitoratge i Manteniment
- Monitoratge i Logging
- Traçabilitat Distribuïda amb OpenTelemetry
- Gestió d'Errors i Recuperació
- Escalabilitat i Rendiment
- SLOs, Alertes i Gestió d'Incidents
Mòdul 7: Seguretat en Microserveis
- Autenticació i Autorització
- Seguretat en la Comunicació
- Pràctiques de Seguretat
- Seguretat en Contenidors i Kubernetes
