En una arquitectura de microserveis, la independència dels serveis no és només qüestió de codi: també afecta les dades. El principi de base de dades per servei estableix que cada microservei ha de ser el propietari exclusiu de les seves dades i ningú més no les pot tocar directament. Aquesta regla, senzilla d'enunciar, dinamita molts costums heretats del món monolític: ja no hi ha un JOIN que creui totes les taules, ni una transacció única que abasti tota l'operació. A canvi, guanyem serveis veritablement autònoms, desplegables i escalables per separat. En aquesta lliçó estudiarem què implica aquest patró, quins problemes crea (consultes i transaccions repartides), i les tècniques per conviure-hi: sharding, replicació i la separació de vistes mitjançant CQRS.
Contingut
- El patró Database per Service
- El problema de les consultes entre serveis
- El problema de les transaccions entre serveis
- Escalat de dades: sharding i replicació
- CQRS per construir vistes de lectura
- Síntesi: com encaixen les peces
- El patró Database per Service
La idea central és que cada servei posseeix la seva pròpia base de dades i és l'única via d'accés a aquestes dades. Un altre servei que necessiti informació l'ha de demanar a través de l'API, mai consultant la base aliena.
┌──────────────────┐ ┌──────────────────┐
│ Servei Comandes │ │ Servei Clients │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ BD │ │ │ │ BD │ │
│ │ comandes │ │ │ │ clients │ │
│ └──────────┘ │ │ └──────────┘ │
└────────┬─────────┘ └─────────┬────────┘
│ API (HTTP / esdeveniments) │
└────────────────────────────┘Beneficis:
- Acoblament dèbil: un servei pot canviar el seu esquema sense trencar els altres.
- Llibertat tecnològica: Comandes pot fer servir PostgreSQL i Clients, MongoDB (persistència poliglota).
- Aïllament de fallades i escalat independent: la càrrega d'un servei no afecta la base de l'altre.
El preu és que perdem les dues grans comoditats del monòlit: les consultes que creuen dades i les transaccions que les abasten. Vegem-les.
- El problema de les consultes entre serveis
En un monòlit, mostrar "les comandes de l'Ana amb el seu nom i email" és un JOIN trivial:
-- Això NOMÉS funciona en un monòlit amb una base única SELECT c.nombre, c.email, p.id, p.total FROM clientes c JOIN pedidos p ON p.cliente_id = c.id WHERE c.id = 1042;
En microserveis aquest JOIN és impossible: clientes i pedidos viuen en bases diferents, possiblement amb motors diferents. Hi ha tres estratègies per resoldre-ho:
2.1 Composició a l'API (API Composition)
Un servei (o el gateway) crida diversos serveis i uneix els resultats en memòria:
public DetallePedidoDto detalle(long pedidoId) {
Pedido p = pedidosClient.obtener(pedidoId); // crida al servei Comandes
Cliente c = clientesClient.obtener(p.clienteId()); // crida al servei Clients
return new DetallePedidoDto(
c.nombre(), c.email(), p.id(), p.total()); // "JOIN" fet en codi
}Senzill, però cada consulta implica diverses crides de xarxa i no escala bé per a llistats grans o filtres complexos (el clàssic problema "N+1" multiplicat per la latència de xarxa).
2.2 Replicació de dades de referència
El servei que necessita la dada manté una còpia local de només lectura de la dada aliena, sincronitzada mitjançant esdeveniments. Per exemple, Comandes desa una còpia del nombre del client, actualitzada quan Clients publica un esdeveniment ClienteActualizado.
2.3 CQRS amb vista materialitzada
Construir una base de lectura dedicada que ja té les dades combinades (ho veurem a l'apartat 5).
- El problema de les transaccions entre serveis
En un monòlit, confirmar una comanda i descomptar l'estoc era una transacció ACID única. En microserveis, "comanda" i "estoc" estan en bases diferents: no existeix una transacció que abasti totes dues. Les transaccions distribuïdes clàssiques (2PC, two-phase commit) existeixen, però són fràgils, lentes i mal vistes en arquitectures modernes perquè bloquegen recursos i redueixen la disponibilitat.
La solució recomanada és el patró Saga (estudiat al Mòdul 5): es descompon l'operació en una seqüència de transaccions locals, cadascuna al seu servei, coordinades per esdeveniments. Si un pas falla, s'executen transaccions de compensació que desfan els anteriors.
Crear Comanda (Comandes) --ok--> Reservar Estoc (Inventari) --ok--> Cobrar (Pagaments)
▲ │ falla
└──── Compensar: cancel·lar comanda ◄─┘La conseqüència conceptual més important: renunciem a la consistència immediata a canvi de consistència eventual. Durant un instant el sistema pot estar "a mitges" (comanda creada, estoc encara no descomptat), i hem de dissenyar l'aplicació per tolerar-ho.
// Pas local + publicació d'esdeveniment, dins d'UNA transacció local
@Transactional
public void crearPedido(ComandoCrearPedido cmd) {
Pedido pedido = Pedido.nuevo(cmd);
repoPedidos.guardar(pedido); // transacció LOCAL del servei Comandes
publicador.publicar(new PedidoCreado(pedido.id(), pedido.lineas()));
// El servei Inventari reaccionarà a PedidoCreado a la SEVA pròpia transacció
}Aquí només es garanteix atomicitat dins del servei Comandes. La coordinació amb Inventari passa de manera asíncrona via l'esdeveniment PedidoCreado. Perquè desar i publicar no es desincronitzin es fa servir el patró Transactional Outbox, que escriu l'esdeveniment a la mateixa transacció local.
- Escalat de dades: sharding i replicació
Quan les dades d'un servei creixen, dues tècniques (complementàries) ajuden a escalar.
4.1 Replicació
Es mantenen còpies de les mateixes dades en diversos nodes. Normalment un és el primari (accepta escriptures) i els altres són rèpliques de només lectura.
# Configuració conceptual de replicació primari-rèplica
database:
primario:
host: db-primario.interno # rep totes les ESCRIPTURES
rol: read-write
replicas:
- host: db-replica-1.interno # serveixen LECTURES, descarreguen el primari
rol: read-only
- host: db-replica-2.interno
rol: read-only- Avantatges: reparteix la càrrega de lectura, millora la disponibilitat (si cau el primari, una rèplica pot promocionar-se).
- Cost: les rèpliques poden anar lleugerament endarrerides (replication lag), introduint consistència eventual a les lectures.
4.2 Sharding (particionat horitzontal)
Es divideix el conjunt de dades entre diversos nodes segons una clau de partició (shard key). Cada node desa un subconjunt diferent; cap node no té tot.
| Estratègia de sharding | Com reparteix | Avantatge | Risc |
|---|---|---|---|
| Per rang | Per intervals de la clau (A-M, N-Z) | Consultes per rang eficients | Punts calents (hotspots) |
| Per hash | hash(clau) % nre. de shards | Repartiment uniforme | Consultes per rang cares |
| Per directori | Taula de cerca explícita | Flexible | El directori és un coll d'ampolla |
shard = hash(cliente_id) % 4 cliente_id=1042 -> shard 2 → node C cliente_id=2001 -> shard 1 → node B
Diferència essencial: la replicació copia les mateixes dades (escala lectures i dóna resiliència); el sharding divideix dades diferents (escala escriptures i volum). En sistemes grans es combinen: cada shard té a més les seves rèpliques.
- CQRS per construir vistes de lectura
CQRS (Command Query Responsibility Segregation) separa el model d'escriptura (comandes que canvien estat) del model de lectura (consultes). El vam introduir al Mòdul 5; aquí l'apliquem al problema de les consultes distribuïdes.
La idea: en lloc de barallar-nos amb JOIN impossibles entre serveis, construïm una vista materialitzada de només lectura que ja combina les dades, alimentada pels esdeveniments que publiquen els serveis.
// Projector: escolta esdeveniments i manté una vista de lectura desnormalitzada
@Component
public class ProyectorResumenPedidos {
private final RepositorioVistaPedido vista;
@EventListener
public void on(PedidoCreado e) {
// Crea/actualitza una fila combinada llesta per consultar
vista.guardar(new ResumenPedido(e.pedidoId(), e.clienteId(), "PENDIENTE"));
}
@EventListener
public void on(ClienteRenombrado e) {
// Manté el nom del client actualitzat a la vista
vista.actualizarNombreCliente(e.clienteId(), e.nuevoNombre());
}
}Què aconseguim:
- La consulta "comandes amb nom del client" es resol llegint una sola taula ja combinada (la vista
ResumenPedido), sense crides entre serveis. - El model de lectura està desnormalitzat i optimitzat per a les consultes reals de la interfície.
- A canvi, la vista s'actualitza de manera asíncrona: és eventualment consistent respecte a les dades d'origen.
-- Consulta sobre la vista materialitzada: ràpida i sense JOIN entre serveis SELECT cliente_nombre, pedido_id, estado FROM resumen_pedidos WHERE cliente_id = 1042;
CQRS no és gratis: duplica dades i afegeix la complexitat de mantenir els projectors. Fes-lo servir quan l'asimetria entre lectures i escriptures ho justifiqui, no per defecte.
Errors habituals i consells
- Compartir la base entre serveis "només per a aquesta consulta". És la porta d'entrada al monòlit distribuït: trenca l'autonomia i reintrodueix acoblament per la base de dades. Prohibit.
- Intentar transaccions ACID globals amb 2PC. Sacrifiquen disponibilitat i escalabilitat. Prefereix Sagas i consistència eventual.
- Triar malament la shard key. Una clau amb distribució desigual (per exemple, "país" quan el 90% són d'un sol país) crea hotspots que anul·len el benefici del sharding.
- Oblidar el replication lag. Si llegeixes d'una rèplica just després d'escriure al primari, pots no veure el teu propi canvi. Per a "read-your-writes" llegeix del primari o fes servir lectures consistents.
- No dissenyar les compensacions de la Saga. Cada pas necessita la seva acció inversa; si l'oblides, una fallada deixa el sistema en estat inconsistent sense remei automàtic.
- Consell: la consistència eventual ha de ser una decisió explícita i comunicada al negoci, no un efecte col·lateral silenciós.
Exercicis
Exercici 1. Explica per què un JOIN entre la taula pedidos (servei Comandes) i la taula clientes (servei Clients) és impossible en una arquitectura de base de dades per servei, i anomena dues maneres de resoldre la necessitat de combinar aquestes dades.
Exercici 2. Tens 100 milions de clients i la base d'un servei no dóna l'abast en escriptures. Replicació o sharding? Proposa una shard key raonable i explica un risc de la teva elecció.
Exercici 3. Descriu, pas a pas, com una Saga gestiona l'operació "crear comanda → reservar estoc → cobrar" quan el cobrament falla.
Solucions
Solució 1. És impossible perquè totes dues taules resideixen en bases de dades diferents, governades per serveis diferents i possiblement amb motors diferents; el motor d'una base no pot executar un JOIN contra taules que no posseeix. Dues solucions: (a) API Composition (cridar tots dos serveis i unir en memòria) i (b) CQRS amb vista materialitzada (mantenir una vista de lectura ja combinada, alimentada per esdeveniments). També és vàlida la replicació local de dades de referència.
Solució 2. Sharding, perquè el problema és de volum d'escriptures i la replicació només escala lectures. Una shard key raonable és hash(cliente_id), que reparteix de manera uniforme. Risc: les consultes que abasten rangs de clients o agregacions globals es tornen cares, perquè han de consultar tots els shards i combinar resultats (scatter-gather).
Solució 3. (1) El servei Comandes crea la comanda en estat PENDENT (transacció local) i publica PedidoCreado. (2) El servei Inventari reserva l'estoc (transacció local) i publica StockReservado. (3) El servei Pagaments intenta cobrar i falla, publicant CobroRechazado. (4) Inventari reacciona a CobroRechazado executant la compensació: allibera l'estoc reservat. (5) Comandes reacciona marcant la comanda com a CANCEL·LADA. El sistema torna a un estat consistent sense haver fet servir una transacció global.
Conclusió
Has après que la base de dades per servei és la peça que fa veritablement autònoms els microserveis, a costa de renunciar als JOIN i a les transaccions globals. Per conviure-hi ja domines un conjunt de tècniques: composició a l'API i CQRS amb vistes materialitzades per a les consultes, Sagas i consistència eventual per a les transaccions, i replicació més sharding per escalar. Totes comparteixen un fil comú: fer més lectures i fer-les ràpides. I quan parlem d'accelerar lectures, l'eina per excel·lència és la caché, amb les seves pròpies estratègies i, sobretot, el seu difícil problema d'invalidació. Això és justament el que veurem a la darrera lliçó del mòdul: Caché i estratègies d'invalidació.
Curs d'Arquitectura d'Aplicacions
Mòdul 1: Fonaments de l'Arquitectura d'Aplicacions
- Què és l'Arquitectura d'Aplicacions?
- El Rol de l'Arquitecte de Programari
- Atributs de Qualitat i Requisits No Funcionals
- Decisions Arquitectòniques i Compromisos (Trade-offs)
- Documentació d'Arquitectura: Vistes i el Model C4
Mòdul 2: Principis i Tàctiques de Disseny
- Acoblament, Cohesió i Separació de Responsabilitats
- Principis SOLID Aplicats a l'Arquitectura
- DRY, KISS, YAGNI i Altres Principis de Disseny
- Tàctiques Arquitectòniques per als Atributs de Qualitat
- Gestió del Deute Tècnic
Mòdul 3: Estils i Patrons Arquitectònics
- Arquitectura Monolítica
- Arquitectura en Capes (N-Tier)
- Arquitectura Client-Servidor
- Arquitectura Hexagonal (Ports i Adaptadors)
- Arquitectura Neta i Ceba (Clean & Onion)
Mòdul 4: Arquitectures Distribuïdes i Microserveis
- Introducció als Sistemes Distribuïts
- Arquitectura de Microserveis
- Descomposició de Serveis i Bounded Contexts
- API Gateway, Service Discovery i Comunicació entre Serveis
- Patrons de Resiliència: Circuit Breaker, Retry i Bulkhead
- El Teorema CAP i la Consistència de Dades
Mòdul 5: Arquitectures Dirigides per Esdeveniments i Missatgeria
- Fonaments de l'Arquitectura Orientada a Esdeveniments
- Missatgeria Asíncrona: Cues i Brokers
- Patrons d'Esdeveniments: Event Sourcing i CQRS
- Gestió de Transaccions Distribuïdes: Patró Saga
- Streaming de Dades en Temps Real
Mòdul 6: Disseny Dirigit pel Domini (DDD)
- Conceptes Fonamentals del DDD
- Disseny Estratègic: Bounded Contexts i Llenguatge Ubic
- Disseny Tàctic: Entitats, Agregats i Repositoris
- Mapatge de Contextos (Context Mapping)
Mòdul 7: Dades i Persistència
- Estratègies de Persistència: SQL vs NoSQL
- Patrons d'Accés a Dades: Repository, Unit of Work i DAO
- Base de Dades per Servei i Gestió de Dades Distribuïdes
- Cau i Estratègies d'Invalidació
Mòdul 8: Arquitectura al Núvol i Desplegament
- Fonaments del Cloud Computing (IaaS, PaaS, SaaS)
- Contenidors i Orquestració amb Docker i Kubernetes
- Arquitectura Serverless
- Patrons de Disseny Cloud-Native
- Infraestructura com a Codi (IaC)
Mòdul 9: Qualitat, Seguretat i Observabilitat
- Escalabilitat: Horitzontal vs Vertical i Balanceig de Càrrega
- Alta Disponibilitat i Tolerància a Fallades
- Seguretat per Disseny i Autenticació/Autorització
- Observabilitat: Logging, Mètriques i Traçabilitat
- Rendiment i Proves de Càrrega
