Tot el que hem construït fins ara és una sola aplicació: un monòlit provat, observable, configurable i conteniritzat. És una arquitectura perfectament respectable i, per a la xarxa municipal de bicicletes de Ribalta, probablement la correcta. Però tard o d'hora algú planteja la pregunta: i si facturació tingués el seu propi cicle de vida? I si l'equip d'estacions desplegués sense coordinar-se amb el de lloguers? I si calgués escalar només la consulta de disponibilitat, que rep cent vegades més trànsit que la resta?

Aquesta lliçó respon a aquestes preguntes amb honestedat. Veurem quin problema resolen realment els microserveis i quin és el seu preu, com es decideix on tallar, què es trenca quan cada servei té la seva pròpia base de dades —els JOIN i les transaccions ACID de 04-07 deixen d'existir— i quins patrons ho substitueixen. Recorrerem l'ecosistema Spring Cloud assenyalant quan sobra, muntarem una passarel·la mínima per a CicloUrbana i acabarem amb una guia de migració progressiva i una llista de comprovació honesta. Avís des de la primera línia: la resposta correcta per a un projecte com CicloUrbana sol ser el monòlit modular, i aquesta lliçó es pren seriosament explicar per què.

Contingut

  1. Què és una arquitectura de microserveis
  2. Monòlit, monòlit modular i microserveis
  3. La llei de Conway i el context delimitat
  4. Descompondre CicloUrbana
  5. Base de dades per servei: què es trenca
  6. Consistència eventual, sagues i el patró outbox
  7. L'ecosistema Spring Cloud
  8. Descobriment de serveis i API Gateway
  9. Configuració centralitzada
  10. Autenticació entre serveis
  11. Comunicació síncrona davant d'asíncrona
  12. Observabilitat distribuïda
  13. Migració progressiva des del monòlit
  14. Errors Comuns i Consells
  15. Exercicis

  1. Què és una arquitectura de microserveis

Un sistema de microserveis és un conjunt de serveis petits, desplegables de manera independent, cadascun propietari de les seves dades i comunicant-se per la xarxa. La definició important és a les tres primeres paraules d'aquesta frase: desplegables de manera independent. Si per publicar un canvi a facturació cal coordinar el desplegament amb lloguers, no hi ha microserveis: hi ha un monòlit distribuït, que combina els inconvenients de tots dos mons.

Els problemes que resol són organitzatius i operatius abans que tècnics:

  • Autonomia d'equips. Cinc equips que despleguen cinc vegades al dia sense trepitjar-se.
  • Escalat selectiu. Vint rèpliques de consulta d'estacions i dues de facturació.
  • Aïllament de fallades. Que la passarel·la de pagaments caigui i les bicicletes se segueixin llogant.
  • Llibertat tecnològica. Un servei de recomanació en Python al costat de serveis en Java 21.
  • Cicles de vida diferents. Un motor de tarifes que canvia cada setmana davant d'un catàleg d'estacions que canvia cada trimestre.

I el preu, que poques vegades s'enuncia amb la mateixa claredat: la complexitat es mou del codi a l'operació. Una crida a un mètode passa a ser una crida de xarxa que pot trigar, fallar o arribar dues vegades. Una transacció passa a ser una coordinació entre serveis. Un NullPointerException passa a ser una investigació als logs de quatre processos. Res d'això no es paga una vegada: es paga cada dia.

  1. Monòlit, monòlit modular i microserveis

Criteri Monòlit Monòlit modular Microserveis
Unitats de desplegament 1 1 N
Fronteres internes Difuses Explícites i verificades Físiques (processos diferents)
Base de dades Una de compartida Una, amb esquemes per mòdul Una per servei
Transaccions ACID (04-07) ACID Consistència eventual, sagues
Consultes entre àrees JOIN JOIN o API interna Crides de xarxa o rèpliques
Escalat Tot o res Tot o res Selectiu per servei
Latència interna Nanosegons Nanosegons Mil·lisegons, i variable
Autonomia d'equips Baixa Mitjana Alta
Cost operatiu Baix Baix Molt alt
Depuració d'una fallada Una pila de crides Una pila de crides Traces distribuïdes (09-06)
Requisits previs Cap Disciplina CI/CD, observabilitat, automatització
Cost d'equivocar-se a la frontera Refactorització Refactorització Redisseny i migració de dades

Aquesta última fila és la raó de l'avís inicial. En un monòlit modular, moure una classe d'un mòdul a un altre és una tarda de feina; entre microserveis, és una migració de dades, un contracte trencat, dos desplegaments coordinats i un període de convivència. I les fronteres gairebé mai no s'encerten a la primera, perquè només s'aprèn on són quan el domini ja es coneix bé.

D'aquí surt la recomanació que sosté la indústria des de fa anys: comença per un monòlit ben modularitzat i extreu serveis quan un dolor concret ho justifiqui. Un dolor concret és «l'equip de facturació no pot desplegar sense nosaltres» o «necessito escalar la consulta d'estacions a vint rèpliques sense escalar la resta», no pas «els microserveis són més moderns».

  1. La llei de Conway i el context delimitat

«Les organitzacions que dissenyen sistemes produeixen dissenys que copien l'estructura de comunicació de la mateixa organització.» — Melvin Conway, 1967

La conseqüència pràctica és brutal: si l'ajuntament de Ribalta té un sol equip, els microserveis que dissenyi acabaran acoblats, perquè no hi ha cap frontera organitzativa que els mantingui separats. I a l'inrevés: si hi ha quatre equips amb responsabilitats clares, l'arquitectura tendirà a alinear-s'hi encara que se'n dissenyi una altra. Dissenyar l'arquitectura és dissenyar l'organització.

Per decidir on tallar, el criteri que funciona ve del disseny guiat pel domini (DDD): el context delimitat, una frontera dins de la qual un terme del negoci té un únic significat. La prova pràctica és preguntar per les paraules:

  • Per a lloguers, una «bicicleta» és un objecte amb estat, bateria i posició actual.
  • Per a facturació, una «bicicleta» és a penes una referència en una línia de factura.
  • Per a manteniment, una «bicicleta» és un actiu amb historial de reparacions i garantia.

Tres models diferents del mateix concepte, cadascun correcte en el seu context. Quan un mateix terme significa coses diferents segons qui el faci servir, allà hi ha una frontera. I a l'inrevés: si dos suposats serveis necessiten constantment el mateix model complet, no eren dos.

On NO és la frontera, i són els dos errors més freqüents:

Tall equivocat Per què falla
Per capa tècnica (servei-controladors, servei-repositoris) Qualsevol canvi funcional toca els tres serveis: acoblament màxim amb cost de xarxa
Per taula (servei-estacions, servei-bicicletes, servei-ancoratges) Fragmenta un mateix concepte de negoci; cada operació necessita tres crides
Per entitat CRUD Produeix serveis anèmics que només serveixen dades, amb la lògica repartida per tot arreu

Un microservei ha de poder respondre a una petició de negoci completa per si sol la major part de les vegades. Si per atendre el seu cas d'ús més comú necessita cridar-ne altres tres, la frontera està mal posada.

  1. Descompondre CicloUrbana

Aplicant el criteri anterior, la xarxa de Ribalta admet quatre candidats raonables:

graph TB
    GW[API Gateway<br/>/api/v1/**]
    GW --> EST[servei-estacions<br/>estacions, ancoratges,<br/>ocupació, bicicletes]
    GW --> ALQ[servei-lloguers<br/>lloguer, devolució,<br/>tarifes, incidències]
    GW --> USU[servei-usuaris<br/>ciutadans, rols,<br/>autenticació]
    FAC[servei-facturacio<br/>cobraments, rebuts,<br/>passarel·la de pagaments]
    ALQ -.->|esdeveniment LloguerFinalitzat| FAC
    ALQ -->|consulta disponibilitat| EST
    EST --> BDE[(BD estacions)]
    ALQ --> BDA[(BD lloguers)]
    USU --> BDU[(BD usuaris)]
    FAC --> BDF[(BD facturació)]
Servei Per què és un context propi Cicle de vida i escala
servei-estacions Model d'infraestructura física; canvia poc Lectura intensiva: l'app mòbil consulta disponibilitat constantment
servei-lloguers El nucli del negoci: iniciar, finalitzar, tarifar Escriptura intensiva, pics en hora punta
servei-usuaris Identitat i autenticació, amb normativa pròpia (RGPD) Estable, poc trànsit
servei-facturacio Vocabulari comptable, integra amb la passarel·la externa Asíncron, tolera retards

Fixa't que les bicicletes viuen dins de servei-estacions i no en un servei propi: el seu cicle de vida està lligat al de la infraestructura, i separar-les obligaria a una crida de xarxa a cada consulta de disponibilitat, que és justament l'operació més freqüent de tot el sistema. És l'aplicació directa de la regla de l'apartat anterior.

I una observació incòmoda: dels quatre, només servei-facturacio té una justificació forta. És el que integra amb un tercer, el que tolera consistència eventual per naturalesa, el que té el vocabulari més diferent i el que menys consultes creuades necessita. Si CicloUrbana hagués d'extreure un sol servei, aquest seria el candidat, i els altres tres podrien continuar sent mòduls del monòlit durant anys.

  1. Base de dades per servei: què es trenca

La regla no negociable dels microserveis és que cada servei és amo exclusiu de les seves dades: ningú més no les llegeix directament. Compartir base de dades entre serveis els acobla en el pitjor punt possible —l'esquema— i anul·la la independència de desplegament: una migració de Flyway (04-08) passaria a requerir la coordinació de tots.

I aquesta regla trenca dues coses que fem servir des de tot el curs:

Els JOIN desapareixen. Aquesta consulta perfectament natural del monòlit:

SELECT l.id, u.nom, e.nom AS estacio, l.import_total
FROM lloguers l
JOIN usuaris u   ON u.id = l.usuari_id
JOIN estacions e ON e.id = l.estacio_origen_id
WHERE l.data_inici >= :desDe;

deixa de ser possible: usuaris i estacions són en altres bases de dades. Les alternatives, amb el seu preu:

Alternativa Com funciona Cost
Composició al client El servei demana els noms als altres dos N+1 sobre la xarxa (04-04, ara amb latència)
Composició a la passarel·la La passarel·la agrega les respostes Lògica de negoci a la infraestructura
Rèplica local de només lectura Cada servei desa còpia de les dades alienes que necessita Consistència eventual, duplicació deliberada
CQRS amb vista materialitzada Un servei de consulta construeix vistes des dels esdeveniments Complexitat alta, molt eficaç en lectura

La tercera és la més usada: servei-lloguers desa el nomEstacio al costat del lloguer, actualitzant-lo quan arriba un esdeveniment EstacioReanomenada. És duplicació deliberada, una cosa que en una base de dades normalitzada seria un error i aquí és la solució correcta.

Les transaccions ACID desapareixen. Tot el mòdul 4 es recolzava en el fet que @Transactional garantia atomicitat: si fallava el cobrament, es desfeia el lloguer. Amb dues bases de dades, això ja no existeix. Les transaccions distribuïdes de dues fases (XA) tècnicament existeixen, però a la pràctica es descarten: bloquegen recursos en diversos sistemes, no escalen i una passarel·la de pagaments externa no hi participa.

  1. Consistència eventual, sagues i el patró outbox

El que substitueix la transacció distribuïda és la consistència eventual: el sistema passa per estats intermedis inconsistents i convergeix. Aplicat a Ribalta: durant uns segons, un lloguer està finalitzat i encara no cobrat. Això no és un defecte tècnic, és una decisió de negoci que cal prendre conscientment: és acceptable que un ciutadà vegi el seu lloguer tancat abans que aparegui el càrrec? Gairebé sempre sí. És acceptable que una bicicleta figuri disponible quan ja no ho és? Gairebé mai.

Una saga és una seqüència de transaccions locals on cada pas publica un esdeveniment que dispara el següent, i cadascun té la seva compensació —no hi ha rollback, hi ha una acció que desfà—.

sequenceDiagram
    participant C as Ciutadà
    participant A as servei-lloguers
    participant B as Bus d'esdeveniments
    participant F as servei-facturacio
    participant P as Passarel·la de pagaments
    C->>A: POST /lloguers/42/finalitzar
    A->>A: tanca lloguer (transacció local) + outbox
    A-->>C: 200 OK — lloguer finalitzat
    A->>B: LloguerFinalitzat(42, 4,80 €)
    B->>F: lliura l'esdeveniment
    F->>P: cobra 4,80 €
    alt cobrament correcte
        F->>B: CobramentRealitzat(42)
        B->>A: marca el lloguer com a cobrat
    else cobrament rebutjat
        F->>B: CobramentRebutjat(42, saldo insuficient)
        B->>A: compensació: marca deute pendent i bloqueja nous lloguers
    end

Coreografiada davant d'orquestrada:

Coreografiada Orquestrada
Com avança Cada servei reacciona a esdeveniments Un coordinador diu a cadascun què ha de fer
Acoblament Baix Mitjà: tots depenen del coordinador
Visibilitat del flux Cap: no és escrit enlloc Explícita, en un sol lloc
Depurar Difícil Més fàcil
Quan fer-la servir 2-3 passos senzills Fluxos llargs, amb moltes compensacions

La coreografiada és la del diagrama i l'adequada per al flux de CicloUrbana. El seu punt feble és seriós: el flux complet no és escrit en cap fitxer; per saber què passa en finalitzar un lloguer cal llegir els escoltadors de quatre serveis. Tan bon punt la saga passa de tres o quatre passos, un orquestrador explícit compensa el seu acoblament.

El patró outbox resol un problema subtil però fatal. Aquest codi està malament:

@Transactional
public void finalitzar(Long lloguerId) {
    lloguer.finalitzar();                          // escriu a PostgreSQL
    broker.publicar(new LloguerFinalitzat(...));   // escriu a Kafka  ← problema
}

Són dos sistemes diferents sense transacció comuna. Si el commit de PostgreSQL falla després de publicar, existeix un esdeveniment d'un lloguer que no es va tancar; si el broker falla després del commit, el lloguer està tancat i ningú no el cobrarà mai. La solució és escriure l'esdeveniment a la mateixa transacció i a la mateixa base de dades, en una taula outbox, i publicar-lo després:

@Transactional
public void finalitzar(Long lloguerId) {
    Lloguer lloguer = repositori.findById(lloguerId).orElseThrow();
    lloguer.finalitzar(Instant.now(rellotge));
    outboxRepositori.save(new MissatgeOutbox(
            "LloguerFinalitzat", lloguerId, json.escriure(esdeveniment)));  // mateixa transacció
}

Un procés a part —una tasca @Scheduled amb ShedLock de 07-03, o un connector de captura de canvis com Debezium— llegeix la taula i publica. Com que pot publicar dues vegades si falla just després d'enviar, el consumidor ha de ser idempotent: això és el que converteix un lliurament «com a mínim una vegada» en un efecte «exactament una vegada».

  1. L'ecosistema Spring Cloud

Peça Què resol Alternativa a Kubernetes (08-04)
Config Server Configuració centralitzada i versionada ConfigMap i Secret
Eureka / Consul Descobriment: on és cada instància Service + DNS intern
Spring Cloud Gateway Punt d'entrada, encaminament, filtres Ingress o una malla de serveis
OpenFeign Client HTTP declaratiu RestClient o @HttpExchange (07-06)
Circuit Breaker Abstracció sobre Resilience4j La mateixa biblioteca, sense l'abstracció
Micrometer Tracing (abans Sleuth) Traces distribuïdes Igual, més una malla de serveis
Stream / Bus Abstracció sobre Kafka o RabbitMQ El client natiu del broker

I aquí va la part que poques vegades es diu. Bona part d'Spring Cloud va néixer abans que Kubernetes fos l'estàndard, per resoldre a l'aplicació problemes que avui resol la plataforma. Si CicloUrbana es desplega a Kubernetes:

  • Eureka sobra: un Service de Kubernetes ja dona un nom DNS estable amb balanceig. Afegir un registre de serveis propi duplica el mecanisme.
  • Config Server sol sobrar: els ConfigMap i els Secret muntats com a fitxers, llegits amb spring.config.import: configtree: (07-02), fan la feina sense un servei més per mantenir.
  • La passarel·la pot sobrar: un Ingress encamina per ruta i per host. Continua tenint sentit quan cal lògica —agregació de respostes, transformació, limitació de taxa per usuari autenticat—.

El que no sobra en cap cas: la resiliència (07-06), l'observabilitat distribuïda (09-06) i la missatgeria. La regla: no afegeixis una peça d'Spring Cloud sense poder anomenar el problema concret que resol i comprovar que la teva plataforma no el resol ja. Cada component és un servei més per desplegar, monitorar, actualitzar i que pot caure.

  1. Descobriment de serveis i API Gateway

El descobriment respon a «en quina adreça és servei-lloguers ara mateix?», una pregunta que no té resposta fixa perquè les instàncies apareixen, desapareixen i canvien d'IP. Un registre —Eureka, Consul o el DNS de Kubernetes— manté aquesta llista, i el client demana «una instància sana de servei-lloguers» en lloc d'una IP.

La passarel·la és l'únic punt d'entrada des de l'exterior. Concentra allò que no té sentit repetir a cada servei: encaminament, terminació TLS, CORS, limitació de taxa i una primera validació del token.

spring:
  cloud:
    gateway:
      routes:
        - id: estacions
          uri: lb://servei-estacions               # lb: resolt pel descobriment
          predicates:
            - Path=/api/v1/estacions/**
          filters:
            - name: CircuitBreaker
              args: { name: cbEstacions, fallbackUri: forward:/reserva/estacions }

        - id: lloguers
          uri: lb://servei-lloguers
          predicates:
            - Path=/api/v1/lloguers/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 20
                redis-rate-limiter.burstCapacity: 40
      default-filters:
        - AddRequestHeader=X-Rastre-Id, ${rastreId}  # coherent amb FiltreRastreig (03-06)

Tres detalls. lb:// delega la resolució al descobriment i reparteix entre instàncies sanes. Els predicates decideixen quines peticions entren a cada ruta —per ruta, capçalera, mètode, host o fins i tot franja horària—. I Spring Cloud Gateway és reactiu, construït sobre WebFlux: no es pot barrejar amb spring-boot-starter-web al mateix projecte, i el seu codi de filtres no ha de bloquejar.

El que la passarel·la no ha de fer: lògica de negoci. Una passarel·la que decideix si un ciutadà pot llogar es converteix en un monòlit encobert pel qual passa tot l'equip, i en un punt únic de fallada amb desplegaments coordinats. Encamina, protegeix i observa; no decideix.

  1. Configuració centralitzada

Amb vint serveis i quatre entorns, la configuració es dispersa. Spring Cloud Config Server la centralitza en un repositori Git i la serveix per HTTP:

# Config Server
spring:
  cloud:
    config:
      server:
        git:
          uri: https://github.com/ajuntament-ribalta/ciclourbana-config
          search-paths: '{application}'
# Cada servei client
spring:
  application:
    name: servei-lloguers
  config:
    import: optional:configserver:http://config:8888

El client demana en arrencar servei-lloguers amb els seus perfils actius (07-02) i rep la configuració combinada, versionada a Git amb historial i revisions de codi. Amb Spring Cloud Bus, un canvi es pot a més refrescar en calent sense reiniciar.

Avantatges reals: una sola font de veritat, historial complet de qui va canviar què i quan, i xifratge de secrets amb {cipher}. Inconvenients igual de reals: un servei més per mantenir i que, si cau, impedeix arrencar a tots els altres; latència afegida a l'arrencada; i la temptació de posar-hi coses que haurien de ser codi. Com es deia a l'apartat 7: a Kubernetes, els ConfigMap solen bastar.

  1. Autenticació entre serveis

El JWT de 05-04 continua sent la peça central, però canvia el repartiment de responsabilitats:

graph LR
    C[Ciutadà] -->|JWT| GW[Gateway<br/>valida signatura i caducitat]
    GW -->|propaga el JWT| ALQ[servei-lloguers<br/>valida un altre cop]
    ALQ -->|propaga el JWT| EST[servei-estacions<br/>valida un altre cop]

Cada servei valida el token pel seu compte, i això no és redundància inútil: és el principi de confiança zero. Si servei-estacions confiés que la passarel·la ja va validar, qualsevol que arribés a aquest servei des de dins de la xarxa —un altre servei compromès, un atacant que ja va entrar— tindria accés total. La validació és barata: comprovar una signatura HS256 són microsegons i no requereix cridar ningú.

El patró s'anomena token relay: la petició sortint porta el mateix Authorization que va arribar, i cada servei actua com a resource server (spring-boot-starter-oauth2-resource-server en un escenari amb OIDC, o el FiltreAutenticacioJwt propi de 05-04). La propagació de la capçalera s'implementa amb un interceptor del client HTTP, que és exactament el que construirem a 07-06.

Per a crides sense usuari —una tasca programada de facturació que consulta lloguers— no hi ha token per propagar. Allà es fa servir el flux de credencials de client: el servei obté el seu propi token amb la seva identitat, no amb la de cap ciutadà. I per a autenticació mútua entre serveis, mTLS, que sol proporcionar la malla de serveis sense tocar el codi.

  1. Comunicació síncrona davant d'asíncrona

Síncrona (REST, gRPC) Asíncrona (RabbitMQ, Kafka)
Qui espera El cridant, bloquejat Ningú
Acoblament temporal Tots dos han de ser vius alhora El receptor pot estar caigut
Propagació de fallades Alta: una caiguda s'encadena Baixa: els missatges esperen a la cua
Latència percebuda Suma de totes les crides Resposta immediata
Consistència Immediata Eventual
Depuració Senzilla: hi ha una pila Difícil: cal seguir missatges
Quan fer-la servir Necessito la resposta per continuar Només necessito comunicar que ha passat alguna cosa

L'última fila és el criteri, i és més útil que qualsevol altra consideració. Aplicat a CicloUrbana:

  • Iniciar un lloguer necessita saber si la bicicleta està disponible → crida síncrona de servei-lloguers a servei-estacions. No es pot continuar sense la resposta.
  • Finalitzar un lloguer no necessita esperar el cobrament → esdeveniment asíncron LloguerFinalitzat que servei-facturacio consumeix quan pot. Si facturació està caiguda una hora, els ciutadans continuen llogant i tornant bicicletes, i els cobraments es processen en tornar.

Aquest segon cas és la millor il·lustració del valor de la missatgeria: converteix la caiguda d'un servei en un retard en lloc d'en una avaria. El preu és la consistència eventual i l'obligació que el consumidor sigui idempotent, perquè els brokers garanteixen «com a mínim una vegada»: si LloguerFinalitzat(42) arriba dues vegades, no es pot cobrar dues vegades. La forma habitual és una taula d'identificadors ja processats, consultada dins de la mateixa transacció del consumidor.

  1. Observabilitat distribuïda

Al monòlit, una fallada deixa una pila de crides completa. Amb quatre serveis, «finalitzar un lloguer triga vuit segons» és una pregunta sense resposta fins que se sap en quin dels quatre se'n van aquests segons. Per això l'observabilitat distribuïda no és un extra: és un requisit previ.

Les tres peces, ja vistes o per veure:

Peça Què aporta On es tracta
Traces Un traceId que travessa tots els serveis i mesura cada tram 09-06
Mètriques Latència, taxa d'error i saturació per servei 09-03 i 09-04
Logs correlacionats Totes les línies d'una operació, a tots els serveis 09-05

Micrometer Tracing propaga el context per les capçaleres traceparent de l'estàndard W3C, i integra l'identificador al MDC, exactament igual que el FiltreRastreig de 03-06 feia dins d'un procés. I l'/actuator/health de 07-01 passa a tenir un paper nou: amb quatre serveis, l'estat agregat del sistema és la unió de quatre sondes, i l'orquestrador retira instàncies pel seu compte.

El criteri de decisió que se'n deriva: si l'equip no té avui traces distribuïdes ni logs centralitzats, no està preparat per a microserveis. No és una qüestió de maduresa abstracta: sense aquestes eines, el primer incident en producció és irresoluble.

  1. Migració progressiva des del monòlit

Ningú no reescriu un monòlit en marxa. El camí que funciona té tres etapes, i les dues primeres aporten valor encara que mai no s'arribi a la tercera.

Etapa 1: modularitzar per dins. Reorganitzar el codi en mòduls amb fronteres explícites, que és el que ja vam començar a 01-04 amb els paquets .estacions, .lloguers, .usuaris, .seguretat i .comu. La diferència és que ara les fronteres es verifiquen: ningú no accedeix a les classes internes d'un altre mòdul, la comunicació passa per la seva API pública o per esdeveniments, i cada mòdul té el seu propi esquema a la base de dades.

Spring Modulith converteix aquesta disciplina en una cosa comprovable: defineix què és un mòdul, permet exposar només allò públic, i ofereix una prova que fa fallar la construcció si un mòdul importa les entranyes d'un altre. Amb ella, l'arquitectura deixa de dependre de la vigilància a les revisions de codi:

class ModularitatTest {
    @Test
    void elsModulsRespectenLesSevesFronteres() {
        ApplicationModules.of(CicloUrbanaApplication.class).verify();
    }
}

Etapa 2: extreure el primer servei amb el patró strangler fig. El nom ve de la figuera estranguladora, que creix al voltant d'un arbre fins a substituir-lo. Aplicat a CicloUrbana, per extreure facturació:

  1. Posar una passarel·la davant del monòlit, que de moment ho encamina tot cap a ell.
  2. Crear servei-facturacio amb la seva base de dades, que duplica la funcionalitat de cobrament.
  3. Enviar-li una còpia dels esdeveniments i comparar resultats sense fer-los servir encara.
  4. Quan coincideixin durant setmanes, moure a la passarel·la el trànsit de facturació al servei nou.
  5. Esborrar el codi de facturació del monòlit.

La clau és als passos 3 i 4: hi ha un període de funcionament en paral·lel en què es pot tornar enrere canviant una ruta. Sense aquesta xarxa, l'extracció és un salt sense paracaigudes.

Etapa 3: repetir només quan faci mal. Cada extracció posterior s'ha de justificar amb un dolor concret i mesurable.

La llista de comprovació honesta. Si no pots respondre «sí» a la majoria, la resposta és el monòlit modular:

  • [ ] Hi ha més d'un equip que es destorba en desplegar?
  • [ ] El lliurament continu està automatitzat, de commit a producció, i triga minuts?
  • [ ] Hi ha traces distribuïdes i logs centralitzats funcionant avui?
  • [ ] La infraestructura es crea de manera automatitzada, sense passos manuals?
  • [ ] L'equip pot operar quatre serveis, amb guàrdies i alertes?
  • [ ] Es coneixen les fronteres del domini prou bé com per encertar?
  • [ ] Hi ha un dolor concret que el monòlit modular no pot resoldre?
  • [ ] S'accepta la consistència eventual a les operacions afectades?

Per a CicloUrbana, amb un equip petit i un domini estable, la resposta sincera és encara no, i això és una conclusió legítima d'aquesta lliçó.

Errors Comuns i Consells

Començar per microserveis sense conèixer el domini. Les fronteres s'encerten quan el domini es coneix, i això arriba després de construir-lo.

Compartir la base de dades entre serveis. És el monòlit distribuït: l'acoblament del monòlit amb la latència de la xarxa i sense les seves transaccions.

Tallar per capes o per taules. Produeix serveis que no poden atendre una petició sense cridar-ne altres tres.

Serveis que només despleguen junts. Si cal coordinar desplegaments, la independència —l'únic benefici real— no existeix.

Publicar l'esdeveniment fora de la transacció. Sense outbox, o hi ha esdeveniments de coses que no van passar o hi ha coses passades sense esdeveniment.

Consumidors no idempotents. Els brokers lliuren «com a mínim una vegada»: un cobrament duplicat és qüestió de temps.

Afegir Spring Cloud sencer «perquè és el que es fa servir». Cada peça és un servei més per operar; a Kubernetes, diverses ja estan resoltes.

Posar lògica de negoci a la passarel·la. Es converteix en un monòlit encobert i en un punt únic de fallada.

Consell: comença pel monòlit modular i verifica'n les fronteres amb Spring Modulith. Aporta el 80 % del benefici amb el 5 % del cost.

Consell: extreu primer el servei menys acoblat, el que integra amb un tercer o tolera consistència eventual. A CicloUrbana, facturació.

Consell: munta l'observabilitat abans que el primer servei. El dia que la necessitis serà tard per instal·lar-la.

Consell: escriu els contractes abans que el codi. OpenAPI (03-07) per a allò síncron i un esquema versionat per als esdeveniments. Un contracte trencat entre serveis es descobreix en producció.

Exercicis

Exercici 1: decidir la frontera

L'ajuntament de Ribalta vol afegir a CicloUrbana un sistema d'abonaments anuals: un ciutadà paga una quota, obté tarifa reduïda durant un any, i l'abonament s'ha de validar en iniciar cada lloguer. Decideix si això justifica un servei propi, un mòdul dins de servei-lloguers o un mòdul del monòlit. Argumenta amb els criteris dels apartats 3 i 4, i descriu com es comunicaria amb la resta en cada cas.

Exercici 2: la saga del lloguer amb abonament

Dissenya el flux complet, com a saga coreografiada, de «finalitzar lloguer amb cobrament i actualització del consum de l'abonament», amb tres participants (servei-lloguers, servei-facturacio, servei-abonaments). Dibuixa el diagrama, defineix els esdeveniments amb les seves dades, indica les compensacions de cada pas i explica com garanteixes que un cobrament no es dupliqui si l'esdeveniment arriba dues vegades.

Exercici 3: revisar una proposta d'arquitectura

Un consultor proposa a l'ajuntament aquesta arquitectura. Troba els problemes i proposa'n una alternativa.

«Dividim CicloUrbana en sis microserveis: ms-controladors (tota la capa REST), ms-negoci (tots els serveis), ms-dades (tots els repositoris), ms-estacions, ms-bicicletes i ms-ancoratges. Els sis comparteixen la base de dades PostgreSQL actual per no duplicar informació i poder continuar fent servir els JOIN. Es despleguen junts des de la mateixa canalització per garantir que les versions són coherents. Fem servir Eureka, Config Server, Gateway, Feign i Bus, tot l'ecosistema Spring Cloud, sobre Kubernetes.»

Solucions

Solució 1

La recomanació és un mòdul dins de servei-lloguers —o, si CicloUrbana continua sent un monòlit, un mòdul més—. El raonament, criteri a criteri:

Prova del vocabulari. Un «abonament» només té significat en el context del lloguer: és un modificador de tarifa. No apareix amb un altre sentit a estacions ni a manteniment. No hi ha dos models diferents del mateix terme, per tant no hi ha frontera de context.

Prova de la petició completa. L'operació més freqüent —iniciar un lloguer— necessita sempre consultar si el ciutadà té abonament vigent. Un servei a part convertiria cada inici de lloguer en una crida de xarxa addicional al camí crític, amb la seva latència, la seva possibilitat de fallada i el seu tallacircuits (07-06). Això és exactament el que l'apartat 3 descriu com a frontera mal posada.

Prova del cicle de vida. Les regles dels abonaments canvien amb la mateixa cadència que les tarifes, i totes dues les decideix la mateixa àrea de l'ajuntament. Res no suggereix cicles de desplegament independents.

Prova organitzativa. No hi ha un equip d'abonaments. Per la llei de Conway, un servei sense equip propi acaba acoblat a qui el mantingui de fet.

On sí que encaixa un servei propi és en el cobrament de la quota anual, que ja pertany a servei-facturacio: emetre el rebut de l'abonament és el mateix vocabulari comptable que cobrar un lloguer, tolera consistència eventual i integra amb la passarel·la externa.

Comunicació resultant. Dins de servei-lloguers, el mòdul d'abonaments exposa una API interna (AbonamentService.tarifaVigent(usuariId, data)) invocada com una crida a mètode, sense xarxa i dins de la mateixa transacció. Cap enfora publica dos esdeveniments: AbonamentContractat, que servei-facturacio consumeix per emetre el rebut, i AbonamentCaducat, que serveix per notificar el ciutadà. Si algun dia hi hagués un equip dedicat i les regles es tornessin molt més complexes, l'extracció seria senzilla precisament perquè el mòdul ja té una frontera explícita: és l'etapa 1 de l'apartat 13 fent la seva feina.

Solució 2

sequenceDiagram
    participant C as Ciutadà
    participant A as servei-lloguers
    participant B as Bus
    participant AB as servei-abonaments
    participant F as servei-facturacio
    C->>A: POST /api/v1/lloguers/42/finalitzar
    A->>A: tanca lloguer + calcula 4,80 € + outbox (una transacció)
    A-->>C: 200 OK
    A->>B: LloguerFinalitzat(id=42, usuari=7, import=4.80, minuts=32)
    B->>AB: lliura
    AB->>AB: descompta 32 min de l'abonament; queden 0,00 € a cobrar
    AB->>B: ConsumAbonamentAplicat(42, importFinal=0.00)
    B->>F: lliura
    alt importFinal = 0
        F->>B: CobramentNoNecessari(42)
    else importFinal > 0 i cobrament correcte
        F->>B: CobramentRealitzat(42, 4.80)
    else cobrament rebutjat
        F->>B: CobramentRebutjat(42, "saldo insuficient")
        B->>AB: compensació: retorna els 32 min a l'abonament
        B->>A: compensació: marca deute pendent i bloqueja nous lloguers
    end

Els esdeveniments i les seves dades. Cadascun porta esdevenimentId (UUID únic), ocorregutEl (instant) i versio de l'esquema, a més de la seva càrrega: LloguerFinalitzat amb lloguerId, usuariId, importBrut i minuts; ConsumAbonamentAplicat amb lloguerId, minutsConsumits i importFinal; CobramentRealitzat i CobramentRebutjat amb lloguerId, importTotal i, aquest últim, motiu.

Les compensacions, que substitueixen el rollback:

Pas Compensació si falla allò posterior
Tancament del lloguer No es desfà: el ciutadà va tornar la bicicleta i això és un fet. Es marca PENDENT_DE_PAGAMENT
Consum de l'abonament MinutsRetornatsAlAbonament(42, 32): es reintegren els minuts
Cobrament Un cobrament correcte es compensa amb una devolució, no amb un esborrat

La primera fila conté la lliçó més important de les sagues: no tot es pot desfer. La compensació no és tornar a l'estat anterior, sinó portar el sistema a un estat consistent nou, que aquí és «lloguer finalitzat amb deute pendent».

La idempotència, que és el que fa segur el lliurament «com a mínim una vegada». A servei-facturacio:

@Transactional
public void alRebre(ConsumAbonamentAplicat esdeveniment) {
    if (processatsRepositori.existsById(esdeveniment.esdevenimentId())) {
        return;                                    // ja tractat: s'ignora
    }
    processatsRepositori.save(new EsdevenimentProcessat(esdeveniment.esdevenimentId(), Instant.now(rellotge)));
    if (esdeveniment.importFinal().signum() > 0) {
        passarela.cobrar(esdeveniment.lloguerId(), esdeveniment.importFinal());
    }
}

Tres detalls que el fan correcte: la comprovació i el registre van a la mateixa transacció que l'efecte, de manera que no pot quedar registrat sense cobrar ni cobrat sense registrar; la clau primària d'EsdevenimentProcessat garanteix la unicitat a la base de dades, així que dos consumidors concurrents no hi passen tots dos; i la taula es purga periòdicament amb una tasca programada de 07-03, perquè no pot créixer indefinidament. Com a salvaguarda addicional, la crida a la passarel·la porta una clau d'idempotència pròpia (el lloguerId), de manera que fins i tot si CicloUrbana demanés dos cobraments, la passarel·la només n'aplicaria un.

Solució 3

La proposta acumula pràcticament tots els errors de la lliçó. Els problemes, ordenats per gravetat:

# Problema Per què és greu
1 Tall per capes (ms-controladors, ms-negoci, ms-dades) Qualsevol canvi funcional —afegir un camp a una estació— toca els tres serveis i exigeix tres desplegaments coordinats. És acoblament màxim pagant latència de xarxa
2 Base de dades compartida Anul·la la independència: una migració de Flyway afecta els sis. És la definició de monòlit distribuït
3 Desplegament conjunt L'únic benefici real dels microserveis era desplegar per separat; si es despleguen junts, s'ha pagat tot el cost sense cap avantatge
4 Tall per taules (ms-estacions, ms-bicicletes, ms-ancoratges) Fragmenta un sol concepte de negoci; consultar la disponibilitat de «Plaça Major» exigiria tres crides de xarxa
5 Solapament de responsabilitats ms-negoci i ms-estacions es trepitgen: no queda clar on viu la lògica d'estacions
6 Tot Spring Cloud sobre Kubernetes Eureka duplica els Service, Config Server duplica els ConfigMap: dos serveis més per operar i que poden caure, sense resoldre res de nou
7 No es menciona l'observabilitat Sense traces distribuïdes, el primer incident entre sis serveis és irresoluble
8 No hi ha justificació No s'anomena cap dolor concret que el monòlit no resolgui

L'alternativa. Etapa 1: deixar CicloUrbana com un sol desplegament, reorganitzat en mòduls verificats amb Spring Modulith —estacions (amb bicicletes i ancoratges a dins, perquè són un sol concepte), lloguers, usuaris, facturacio— cadascun amb el seu esquema a PostgreSQL i comunicant-se per API pública o esdeveniments d'Spring. Això dona fronteres reals, límits comprovats per la construcció i cost operatiu zero.

Etapa 2: muntar l'observabilitat —traces, mètriques i logs centralitzats (09-03 a 09-06)— i el lliurament continu (08-05). Són requisit previ, no conseqüència.

Etapa 3: si i només si apareix un dolor concret, extreure servei-facturacio amb el patró strangler fig, amb la seva pròpia base de dades, comunicant-se per esdeveniments amb outbox i consum idempotent. Un servei, no sis.

D'Spring Cloud, sobre Kubernetes, quedaria només allò que la plataforma no dona: Resilience4j per a la resiliència (07-06), Micrometer Tracing per a les traces (09-06) i, si la missatgeria ho justifica, el client del broker. Ni Eureka, ni Config Server, ni Bus.

El resum que se li retornaria al consultor: la proposta té el cost operatiu de sis microserveis, l'acoblament d'un monòlit i cap dels beneficis de cap de les dues arquitectures.

Conclusió

Aquesta lliçó ha estat tant sobre quan no fer servir microserveis com sobre com fer-los servir. Saps que la seva definició operativa és desplegable de manera independent, i que si dos serveis s'han de desplegar junts no són microserveis sinó un monòlit distribuït. Tens la taula que compara monòlit, monòlit modular i microserveis en onze dimensions, amb la fila decisiva: equivocar-se en una frontera costa una tarda en un mòdul i una migració de dades entre serveis. Coneixes la llei de Conway i les seves conseqüències, i el criteri que sí que funciona per decidir on tallar —el context delimitat, detectat per la prova del vocabulari— juntament amb els dos talls que no funcionen mai: per capa tècnica i per taula.

Has descompost CicloUrbana en servei-estacions, servei-lloguers, servei-usuaris i servei-facturacio, entenent per què les bicicletes es queden dins d'estacions i per què, dels quatre, només facturació té una justificació forta. Saps què es trenca amb una base de dades per servei —els JOIN i les transaccions ACID de 04-07— i què ho substitueix: composició, rèpliques locals de només lectura com a duplicació deliberada, consistència eventual, sagues coreografiades i orquestrades amb les seves compensacions, i el patró outbox que evita la fallada subtil d'escriure a la base de dades i al broker sense transacció comuna, amb la idempotència del consumidor com a peça que converteix «com a mínim una vegada» en «exactament una vegada».

Coneixes l'ecosistema Spring Cloud peça a peça i, més important, quan sobra: a Kubernetes, Eureka duplica els Service i Config Server duplica els ConfigMap. Saps què aporten el descobriment i la passarel·la, has vist la configuració mínima d'Spring Cloud Gateway encaminant /api/v1/estacions/** i /api/v1/lloguers/**, i tens clar que la passarel·la encamina i protegeix però no decideix. Entens el token relay i per què cada servei valida el JWT pel seu compte, el criteri per triar entre comunicació síncrona i asíncrona —necessito la resposta per continuar?—, i per què l'observabilitat distribuïda no és un extra sinó un requisit previ. I tens la guia de migració: modularitzar primer amb Spring Modulith verificant les fronteres a la construcció, extreure després el primer servei amb el patró strangler fig i el seu període de funcionament en paral·lel, i la llista de comprovació que per a CicloUrbana dona avui un «encara no» perfectament legítim.

Queda pendent allò més pràctic de tot. Hem dit que servei-lloguers crida servei-estacions, que cal propagar el JWT i l'identificador de rastre, que fa falta un tallacircuits a la passarel·la i que la caiguda d'un servei no s'ha d'encadenar. Res d'això no ho hem escrit encara, i continua fent falta encara que CicloUrbana no es divideixi mai: tan bon punt l'aplicació crida la passarel·la de pagaments de Ribalta —un sistema extern que pot estar lent, caigut o retornar errors—, tots aquests problemes apareixen exactament igual. La lliçó següent, Comunicació entre Serveis i Tolerància a Fallades, els resol amb codi: RestClient i les interfícies declaratives, temps d'espera que no es poden oblidar, interceptors que propaguen el rastre i el token, i Resilience4j amb els seus reintents, tallacircuits, limitadors, mampares i degradació elegant, tot provat amb un servidor simulat que fingeix estar lent, caigut i trencat.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

Mòdul 2: Conceptes bàsics de Spring Boot

Mòdul 3: Construint serveis web RESTful

Mòdul 4: Accés a dades amb Spring Boot

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

Mòdul 7: Funcions avançades de Spring Boot

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats