A la lliçó anterior vam ordenar el monòlit de PideYa per dins: capes netes, ports, adaptadors, repositoris. Però l'èxit porta un problema nou: l'equip de repartiment no pot desplegar sense coordinar-se amb el de pagaments, un pic de trànsit al catàleg obliga a escalar tota l'aplicació, i cada release és un esdeveniment. La resposta de la indústria és partir el sistema en microserveis: processos petits, desplegables per separat, que es comuniquen per xarxa. Aquesta decisió no surt de franc — cada crida a mètode es converteix en una crida remota que pot fallar — i per això existeix un catàleg propi de patrons. Com veuràs, molts són vells coneguts del GoF estirats sobre la xarxa.
Contingut
- Com partir el monòlit: bounded contexts
- API Gateway i Backend for Frontend
- Service Registry i Discovery
- Circuit Breaker
- Retry amb backoff i idempotència
- Saga: transaccions entre serveis
- Strangler Fig: migrar sense big bang
- Database per Service i les seves conseqüències
Com partir el monòlit: bounded contexts
El primer error possible és partir malament. Els talls no es fan per capes tècniques ("servei de base de dades", "servei de lògica") sinó per capacitats de negoci. El Domain-Driven Design (DDD, Eric Evans) — que aquí només esmentem — anomena bounded context cada frontera dins de la qual un model té un significat coherent. A PideYa, "plat" significa coses diferents per al catàleg (foto, descripció, al·lèrgens) i per a cuina (temps de preparació, estació): són contextos diferents.
El tall de PideYa queda així:
| Servei | Responsabilitat | Patrons GoF que s'emporta del monòlit |
|---|---|---|
| Catàleg | Carta, preus, disponibilitat | Composite (carta), Flyweight, Visitor |
| Comandes | Cicle de vida de la comanda | State, Builder, Chain de validació |
| Pagaments | Cobraments i reemborsaments | Adapter/Abstract Factory de passarel·les |
| Repartiment | Assignació i seguiment | Mediator (CentralRepartiment), Strategy |
| Notificacions | Email, SMS, push | Bridge, Decorator (reintents, logging) |
Fixa-t'hi: cada servei s'emporta amb ell els patrons interns que vam construir als mòduls 2-4. Els microserveis canvien l'entre, no el dins.
API Gateway i Backend for Frontend
Amb cinc serveis, l'app mòbil ha de conèixer cinc URLs, cinc esquemes d'autenticació, cinc formats d'error? No: s'hi anteposa un API Gateway, un únic punt d'entrada que enruta, autentica, limita trànsit i agrega respostes.
Reconeixes la intenció? És la Facade de 03-06 a escala de xarxa: ocultar un subsistema complex rere una interfície simple. FacanaCheckout simplificava crides entre objectes; el gateway simplifica crides entre processos.
flowchart LR
APP[App client] --> GW[API Gateway]
WEB[Web] --> GW
RID[App repartidor] --> BFF[BFF repartidor]
GW --> CAT[Cataleg]
GW --> PED[Comandes]
GW --> PAG[Pagaments]
BFF --> PED
BFF --> REP[Repartiment]
Quan cada tipus de client necessita agregacions molt diferents (l'app del repartidor vol rutes i comandes assignades; la web del client vol carta i seguiment), un únic gateway s'engreixa fins a convertir-se en un coll d'ampolla d'equip. El patró Backend for Frontend (BFF) crea un gateway per tipus de client, mantingut per l'equip d'aquell frontend.
Service Registry i Discovery
Al monòlit, cridar el mòdul de pagaments era passarella.cobrar(...). Ara Pagaments són tres instàncies les IPs de les quals canvien amb cada desplegament. El Service Registry (Eureka, Consul, o el DNS de Kubernetes) és un directori on cada instància es registra en arrencar i del qual els clients obtenen adreces fresques (discovery). És la vella idea del desacoblament per indirecció: ningú no referencia instàncies concretes, igual que a Factory Method ningú no referenciava classes concretes — aquí la "fàbrica" et fabrica adreces.
Circuit Breaker
La crida remota introdueix un mode de fallada nou: el servei lent. Si Pagaments triga 30 segons a respondre, cada checkout reté un fil 30 segons, els fils s'esgoten i Comandes cau també: fallada en cascada. El Circuit Breaker (popularitzat per Nussbaumer/Nygard a Release It!) actua com un fusible:
stateDiagram-v2
[*] --> Closed
Closed --> Open : fallades >= llindar
Open --> HalfOpen : passa el temps d'espera
HalfOpen --> Closed : crida de prova OK
HalfOpen --> Open : crida de prova falla
Closed --> Closed : exit (reseteja comptador)
I aquesta màquina d'estats no et recorda res? És el patró State de 04-09 aplicat a la salut d'una connexió. Una implementació simplificada:
public class CircuitBreaker {
private enum Estat { CLOSED, OPEN, HALF_OPEN }
private Estat estat = Estat.CLOSED;
private int falladesSeguides = 0;
private long obertDes = 0;
private final int llindarFallades; // p. ex. 5
private final long esperaMs; // p. ex. 10_000
public CircuitBreaker(int llindarFallades, long esperaMs) {
this.llindarFallades = llindarFallades;
this.esperaMs = esperaMs;
}
public synchronized <T> T executar(Supplier<T> crida, Supplier<T> fallback) {
if (estat == Estat.OPEN) {
if (System.currentTimeMillis() - obertDes < esperaMs) {
return fallback.get(); // circuit obert: ni ho intentem
}
estat = Estat.HALF_OPEN; // deixem passar UNA crida de prova
}
try {
T resultat = crida.get();
estat = Estat.CLOSED; // exit: el servei s'ha recuperat
falladesSeguides = 0;
return resultat;
} catch (Exception e) {
falladesSeguides++;
if (estat == Estat.HALF_OPEN || falladesSeguides >= llindarFallades) {
estat = Estat.OPEN;
obertDes = System.currentTimeMillis();
}
return fallback.get();
}
}
}Punts a entendre del codi: en OPEN fallem ràpid amb un fallback (per exemple, "desem la teva comanda i et cobrarem d'aquí a uns minuts") en lloc d'esperar un timeout; HALF_OPEN deixa passar una crida-sonda per comprovar si el servei ha reviscut. En producció usaries Resilience4j, que embolcalla la crida igual que un Decorator (03-05) — de fet la seva API es diu literalment CircuitBreaker.decorateSupplier.
Retry amb backoff i idempotència
Moltes fallades de xarxa són transitòries: reintentar sol bastar. Però reintentar malament és pitjor que no reintentar:
- Backoff exponencial: espera 100 ms, després 200, 400, 800... per no rematar un servei que s'està recuperant. Afegeix-hi jitter (aleatorietat) perquè mil clients no reintentin sincronitzats.
- Idempotència: si reintentes
cobrar(comanda)perquè no ha arribat la resposta... i si el primer cobrament sí que es va executar? Cobrament doble. La solució és que l'operació sigui idempotent: executar-la dues vegades té l'efecte d'una. A PideYa, cada cobrament porta una clau d'idempotència (l'id de la comanda); si Pagaments rep dues vegades la mateixa clau, la segona vegada retorna el resultat desat sense cobrar de nou.
Regla pràctica: mai no activis reintents sobre una operació no idempotent. Aquest duet reapareixerà amb la missatgeria a 06-03.
Saga: transaccions entre serveis
Al monòlit, el checkout era una transacció de BD: comanda + cobrament + estoc, tot o res (la Unit of Work de la lliçó anterior). Amb Database per Service ja no hi ha transacció que abasti Comandes, Pagaments i Repartiment. El patró Saga la substitueix per una seqüència de transaccions locals, on cada pas que falla dispara compensacions que desfan els passos anteriors.
| Estil | Com funciona | Recorda a |
|---|---|---|
| Coreografia | Cada servei escolta esdeveniments i reacciona; ningú no dirigeix | Observer encadenat; simple amb pocs passos, il·legible amb molts |
| Orquestració | Un orquestrador de saga diu a cada servei què fer i n'espera resposta | Mediator (04-06): centralitza la conversa, com CentralRepartiment |
Saga orquestrada del checkout de PideYa:
- Orquestrador → Comandes: crear comanda (local, confirmada).
- Orquestrador → Pagaments: cobrar. Si falla → compensar: Comandes.cancelarComanda().
- Orquestrador → Repartiment: assignar repartidor. Si falla → compensar: Pagaments.reemborsar(), Comandes.cancelarComanda().
Les compensacions són la intenció de l'undo de Command (04-03) a escala de sistema: cada pas defineix la seva operació inversa. Compte: no és cap rollback màgic — el cobrament va passar i el reemborsament és una operació de negoci nova, visible per al client.
Strangler Fig: migrar el monòlit
PideYa no reescriu el monòlit de cop (el "big bang" gairebé sempre acaba malament). El patró Strangler Fig (Fowler) — anomenat així per la figuera estranguladora que creix envoltant un arbre fins a substituir-lo — consisteix en:
- Posar una façana d'enrutament (el mateix API Gateway serveix) davant del monòlit.
- Extreure una capacitat (p. ex. Notificacions, la més desacoblada gràcies al Bridge de 03-03) a un servei nou.
- Redirigir al gateway aquell trànsit cap al servei nou; la resta continua anant al monòlit.
- Repetir fins que el monòlit quedi buit... o fins que deixi de fer mal, que de vegades arriba abans.
És l'estratègia de refactorització de 05-04 elevada a arquitectura: passos petits, sistema sempre funcionant, i els tests de caracterització ara són tests de contracte entre serveis.
Database per Service
Cada servei posseeix la seva base de dades i ningú més no la toca; els altres hi accedeixen només per la seva API. Sense això, els microserveis són un monòlit distribuït: la BD compartida acobla els esquemes i els desplegaments.
Conseqüències que cal acceptar (no són errors, són el preu):
- No hi ha JOIN entre serveis: la pantalla "les meves comandes amb noms de plats" exigeix compondre dades de Comandes i Catàleg (al gateway/BFF, o duplicant dades).
- No hi ha transaccions globals: d'aquí les sagues.
- Consistència eventual: les dades duplicades triguen a convergir — ho tractarem a fons a la lliçó següent.
- A canvi: cada servei tria el seu emmagatzematge (Catàleg pot usar un documental; Pagaments, relacional estricte) i desplega el seu esquema sense coordinar-se.
Errors Comuns i Consells
- Començar per microserveis: per a un equip petit, el monòlit ben organitzat de la lliçó 06-01 és més ràpid i barat. Els microserveis resolen problemes d'escala organitzativa (molts equips, desplegaments independents). "Espera fins que faci mal" també s'aplica aquí.
- El monòlit distribuït: serveis que comparteixen BD o que s'han de desplegar junts. Tens tots els costos de la xarxa i cap avantatge. Símptoma: una història d'usuari típica toca tres serveis.
- Nanoserveis: tallar massa fi (un servei per entitat) multiplica crides de xarxa i sagues. El tall correcte segueix els bounded contexts, no les taules.
- Reintents sense idempotència: el clàssic cobrament duplicat. Dissenya la clau d'idempotència abans d'activar reintents.
- Circuit breaker sense fallback pensat: obrir el circuit i retornar un error 500 pelat amb prou feines millora res. El valor és al pla B de negoci (degradar, encuar, respondre amb memòria cau).
- Saga sense dissenyar les compensacions: si
reemborsar()no existeix o pot fallar sense pla, la saga deixa el sistema en un estat intermedi invisible. Les compensacions són requisits de negoci de primera classe.
Exercicis
- Detecta el mal tall. Un arquitecte proposa aquests serveis per a PideYa:
servei-entitats(totes les classes de domini),servei-logica(tots els casos d'ús) iservei-dades(tot l'accés a BD). Explica per què és un mal tall i quin criteri s'hauria d'usar. - Traça la saga. El client demana amb un cupó de fidelització (mòdul del cas pràctic de 05-02). La saga és: crear comanda → bescanviar cupó (servei Fidelització) → cobrar la quantia amb descompte → assignar repartiment. Enumera les compensacions necessàries si falla (a) el cobrament i (b) l'assignació de repartiment.
- Raona el circuit breaker. Amb el
CircuitBreakerde la lliçó configurat ambllindarFallades=3iesperaMs=10000: partint deCLOSED, arriben aquestes crides: fallada, fallada, èxit, fallada, fallada, fallada, (passen 4 s) crida, (passen 7 s més) crida amb èxit. Indica l'estat després de cada pas i què rep cada cridador.
Solucions
- És un tall per capes tècniques, no per capacitats de negoci: qualsevol funcionalitat nova ("afegir propines") tocaria els tres serveis alhora, obligant a desplegar-los coordinats — un monòlit distribuït. El criteri correcte són els bounded contexts: fronteres dins de les quals un model és coherent i un equip pot treballar i desplegar de forma autònoma (catàleg, comandes, pagaments, repartiment, notificacions).
- (a) Falla el cobrament: compensar en ordre invers → Fidelitzacio.retornarCupo() i Comandes.cancelarComanda(). (b) Falla el repartiment: Pagaments.reemborsar(), Fidelitzacio.retornarCupo(), Comandes.cancelarComanda(). Observa que les compensacions s'executen en ordre invers al dels passos, exactament com una pila d'undo de Command; i que "retornar un cupó ja bescanviat" ha d'existir com a operació de negoci.
- Fallada (1/3, CLOSED) → fallada (2/3, CLOSED) → èxit (comptador a 0, CLOSED) → fallada (1/3) → fallada (2/3) → fallada (3/3 → OPEN, s'anota l'instant). Als 4 s: continua en OPEN (4000 < 10000), el cridador rep el fallback sense intentar la crida. Als 11 s de l'obertura: passa a HALF_OPEN, s'executa la crida de prova, té èxit → CLOSED i comptador a 0; el cridador rep el resultat real.
Conclusió
PideYa ja és una plataforma: cinc serveis tallats per bounded contexts, un gateway que els amaga (Facade), un registre que els localitza, fusibles que n'aïllen les fallades (State), reintents idempotents i sagues que substitueixen les transaccions (Mediator + undo de Command), tot migrat gradualment amb Strangler Fig. Però hem donat per fet el més fràgil: la comunicació mateixa. Què passa quan un missatge es perd, es duplica o arriba tard? Com es propaga l'esdeveniment "comanda confirmada" a cinc serveis sense cridar-los un a un? Aquest és el territori de les cues, els brokers i la consistència eventual: Patrons de Disseny en Sistemes Distribuïts.
Curs de Patrons de Disseny de Programari
Mòdul 1: Introducció als Patrons de Disseny
- Què són els Patrons de Disseny?
- Història i Origen dels Patrons de Disseny
- Principis de Disseny: SOLID i Altres Fonaments
- UML Essencial per Entendre Patrons
- Classificació dels Patrons de Disseny
- Avantatges i Desavantatges d'Usar Patrons de Disseny
Mòdul 2: Patrons Creacionals
- Introducció als Patrons Creacionals
- Singleton
- Factory Method
- Abstract Factory
- Builder
- Prototype
- Comparativa i Elecció de Patrons Creacionals
Mòdul 3: Patrons Estructurals
- Introducció als Patrons Estructurals
- Adapter
- Bridge
- Composite
- Decorator
- Facade
- Flyweight
- Proxy
- Comparativa i Elecció de Patrons Estructurals
Mòdul 4: Patrons de Comportament
- Introducció als Patrons de Comportament
- Chain of Responsibility
- Command
- Interpreter
- Iterator
- Mediator
- Memento
- Observer
- State
- Strategy
- Template Method
- Visitor
- Comparativa i Elecció de Patrons de Comportament
Mòdul 5: Aplicació de Patrons de Disseny
- Com Seleccionar el Patró Adequat
- Exemples Pràctics d'Ús de Patrons
- Patrons de Disseny en Projectes Reals
- Refactorització Usant Patrons de Disseny
- Antipatrons: Quan els Patrons es Tornen un Problema
Mòdul 6: Patrons de Disseny Avançats
- Patrons de Disseny en Arquitectures Modernes
- Patrons de Disseny en Microserveis
- Patrons de Disseny en Sistemes Distribuïts
- Patrons de Concurrència
- Patrons de Disseny en Desenvolupament Àgil
