Durant deu mòduls hem construït la xarxa de bicicletes elèctriques de Ribalta capa per capa. Primer un endpoint que retornava quatre estacions en memòria; després el contenidor de Spring i els seus beans; el contracte REST amb els seus DTOs i els seus errors; la persistència sobre PostgreSQL amb Flyway; la seguretat amb JWT i regles per dada; les proves; l'operabilitat; el desplegament; i per fi l'observabilitat, el criteri i la neteja. Cada lliçó va afegir una peça i va mirar gairebé sempre cap a la següent.

Mai no hem mirat CicloUrbana sencera. Aquesta lliçó ho fa: l'arquitectura completa en un diagrama, l'estructura real del repositori, el viatge d'una sola petició des del balancejador fins a la mètrica —citant a cada pas on es va estudiar—, el mapa de què va construir cada mòdul, les decisions de disseny amb les seves alternatives i les seves contrapartides, el pom.xml i la configuració finals, com posar el projecte en marxa des de zero i cap a on continuar portant-lo. Si has seguit el curs, aquí ho reconeixeràs tot; el que és nou és veure-ho junt.

Contingut

  1. L'arquitectura final
  2. El repositori complet
  3. El viatge d'una petició: POST /api/v1/lloguers
  4. Mapa del curs: què va construir cada mòdul
  5. Decisions de disseny i les seves alternatives
  6. El pom.xml final
  7. La configuració final
  8. Posar en marxa el projecte des de zero
  9. Recorregut d'extrem a extrem amb peticions .http
  10. Llista de comprovació final de posada en producció
  11. Cap a on continuar amb aquest projecte
  12. Errors Comuns i Consells
  13. Exercicis

  1. L'arquitectura final

flowchart TB
    subgraph CLI["Clients"]
        M["App mòbil<br/>ciutadans de Ribalta"]
        P["Panell de l'ajuntament<br/>operaris i administració"]
    end

    subgraph BORDE["Vora"]
        ALB["ALB / Ingress<br/>TLS, límit de taxa · 08-03, 08-04"]
    end

    subgraph APP["CicloUrbana · 3 rèpliques · 07-04, 08-04"]
        F["Cadena de filtres<br/>FiltreRastreig · FiltreAutenticacioJwt · 03-06, 05-04"]
        C["Controladors REST<br/>DTOs i validació · 03-02, 03-05"]
        S["Serveis<br/>@Transactional · @PreAuthorize · 04-07, 05-05"]
        R["Repositoris<br/>Spring Data JPA · 04-05"]
        T["Tasques i asincronia<br/>@Scheduled · @Async · 07-03"]
        AC["Actuator<br/>sondes, mètriques · 07-01, 09-03"]
        F --> C --> S --> R
        S --> T
    end

    subgraph DAT["Dades"]
        BD[("PostgreSQL 16<br/>esquema amb Flyway · 04-08")]
        RD[("Redis<br/>memòria cau distribuïda · 09-02")]
    end

    subgraph EXT["Serveis externs"]
        PG["Passarel·la de pagaments<br/>RestClient + Resilience4j · 07-06"]
    end

    subgraph OBS["Observabilitat · mòdul 9"]
        PR["Prometheus"]
        LO["Loki"]
        TE["Tempo"]
        OT["OTel Collector"]
        GR["Grafana"]
        OT --> TE
        PR --> GR
        LO --> GR
        TE --> GR
    end

    M --> ALB
    P --> ALB
    ALB --> F
    R --> BD
    S --> RD
    T --> PG
    AC -.->|"scrape /actuator/prometheus"| PR
    APP -.->|"logs JSON a stdout"| LO
    APP -.->|"OTLP"| OT

Quatre observacions sobre el dibuix, perquè un diagrama sense lectura és decoració.

L'aplicació és un sol desplegable. Els cinc blocs de dins no són serveis: són capes i mòduls dins del mateix procés. És el monòlit modular de 07-05, i les fletxes entre ells són crides a mètodes, no crides de xarxa.

Les dependències apunten cap endins i cap avall. Filtres → controladors → serveis → repositoris → base de dades. Cap fletxa no puja. És la regla de dependència de 10-01, verificada automàticament amb les regles d'ArchUnit de 10-03.

L'observabilitat és un lateral, no una capa. No és al camí de la petició: l'aplicació publica mètriques, logs i traces, i uns altres els recullen. Si Prometheus cau, CicloUrbana continua funcionant.

L'única cosa que pot tombar el servei és PostgreSQL. La memòria cau de Redis degrada a Caffeine, la passarel·la té tallacircuits i respatller de cobrament diferit, i la resta és opcional. Aquesta asimetria és deliberada i és darrere del fet que la sonda readiness sí que consulti la base de dades i la de liveness no.

  1. El repositori complet

ciclourbana/
├── .github/workflows/
│   ├── ci.yml                      # compilar, provar, JaCoCo, Spotless · 08-05
│   └── cd.yml                      # imatge, Trivy, desplegament a pre i prod · 08-05
├── helm/ciclourbana/
│   ├── Chart.yaml · values.yaml · values-pre.yaml · values-prod.yaml
│   └── templates/                  # deployment, service, ingress, hpa, job-migracio · 08-04
├── observabilitat/                 # prometheus.yml, loki.yml, tempo.yml,
│                                   # otel-collector.yml, taulers de Grafana · 09-04, 09-06
├── carrega/carrega-hora-punta.js   # escenari de k6 · 09-01
├── peticions/ciclourbana.http      # col·lecció d'extrem a extrem · apartat 9
├── src/main/java/com/ciclourbana/
│   ├── CicloUrbanaApplication.java          # arrel de l'escaneig · 01-04
│   ├── comu/                                # transversal
│   │   ├── ConfiguracioComuna.java          # Clock, RestClient.Builder · 02-01
│   │   ├── EntitatAuditable.java            # createdAt/By, updatedAt/By · 04-03
│   │   ├── PaginaResponse.java              # embolcall propi de Page · 04-05
│   │   ├── FiltreRastreig.java              # rastreId en MDC i capçalera · 03-06, 09-06
│   │   ├── GestorGlobalExcepcions.java      # ProblemDetail RFC 7807 · 03-06
│   │   ├── CicloUrbanaException.java + jerarquia · 03-06
│   │   └── MetriquesCicloUrbana.java        # mètriques de negoci · 09-03
│   ├── estacions/                           # Estacio, Repositori, Service,
│   │   └── dto/                             # Controller, Mapper + DTOs · mòduls 3 i 4
│   ├── bicicletes/                          # Bicicleta, EstatBicicleta,
│   │   └── dto/                             # @MatriculaBicicleta, RevisorFlota
│   ├── lloguers/                            # Lloguer, CalculadoraTarifa i les tres
│   │   └── dto/                             # tarifes, SelectorTarifa, CaducadorLloguers
│   ├── usuaris/                             # Usuari, rols, moneder
│   ├── seguretat/                           # ConfiguracioSeguretat, ServeiJwt,
│   │                                        # FiltreAutenticacioJwt, UsuariAutenticat,
│   │                                        # SeguretatLloguers, AuditoriaSeguretat · mòdul 5
│   ├── pagaments/                           # ClientPassarelaPagaments + Resilience4j · 07-06
│   └── config/                              # ConfiguracioTasques, ConfiguracioAsincronia,
│                                            # ConfiguracioCache, ConfiguracioOpenApi
├── src/main/resources/
│   ├── application.yml                      # comú a tots els entorns · 07-02
│   ├── application-{dev,test,pre,prod}.yml  # només les diferències
│   ├── logback-spring.xml                   # JSON a stdout amb rastreId · 09-05
│   └── db/migration/V1__…V9__…sql           # esquema versionat · 04-08
├── src/test/java/com/ciclourbana/           # mirall de l'arbre de producció · 06-01
│   ├── …Test.java                           # unitàries i llesques (Surefire)
│   ├── …IT.java                             # integració amb Testcontainers (Failsafe)
│   ├── ProvaIntegracioBase.java             # context compartit · 06-05
│   └── ReglesArquitecturaTest.java          # ArchUnit · 10-03
├── Dockerfile                               # multietapa, usuari ciclo · 07-04
├── docker-compose.yml                       # app + PostgreSQL + Redis
├── docker-compose.observabilitat.yml        # Prometheus, Grafana, Loki, Tempo, Collector
├── .dockerignore · .gitignore · .env.exemple
├── mvnw · mvnw.cmd · .mvn/
└── pom.xml

Dues coses mereixen atenció. src/test és un mirall exacte de src/main, cosa que permet accedir a membres amb visibilitat de paquet sense obrir la classe de producció i fa l'arbre navegable. I la separació *Test / *IT no és cosmètica: Surefire executa els primers a ./mvnw test en segons, i Failsafe els segons a ./mvnw verify amb PostgreSQL real.

  1. El viatge d'una petició: POST /api/v1/lloguers

La Marta obre l'aplicació a l'estació «Plaça Major», tria una bicicleta i prem «llogar». Això és el que passa.

sequenceDiagram
    autonumber
    participant M as App mòbil
    participant L as ALB / Ingress
    participant FS as Filtres de Spring Security
    participant DS as DispatcherServlet
    participant CT as LloguerController
    participant SV as LloguerService (proxy)
    participant BD as PostgreSQL
    participant EV as NotificadorLloguer
    participant PG as Passarel·la de pagaments

    M->>L: POST /api/v1/lloguers · Bearer …
    L->>FS: TLS acabat · X-Forwarded-*
    FS->>FS: FiltreRastreig: span arrel + rastreId al MDC
    FS->>FS: FiltreAutenticacioJwt: valida signatura i exp
    FS->>DS: SecurityContext amb UsuariAutenticat
    DS->>CT: binding a IniciarLloguerRequest
    CT->>CT: @Valid: matrícula, ids, tarifa
    CT->>SV: iniciar(peticio)
    SV->>SV: @PreAuthorize: isAuthenticated()
    SV->>BD: BEGIN (READ_COMMITTED)
    SV->>BD: SELECT … FOR UPDATE (millor bicicleta)
    SV->>SV: SelectorTarifa: calcula l'import
    SV->>BD: INSERT lloguer · UPDATE bicicleta
    SV->>BD: COMMIT
    SV-->>CT: LloguerResponse (mapejat dins de la tx)
    CT-->>M: 201 Created · Location · X-Rastre-Id
    Note over EV,PG: AFTER_COMMIT, ja fora del fil HTTP
    SV->>EV: esdeveniment LloguerIniciat
    EV->>PG: autoritzar cobrament (@Async + tallacircuits)

El detall pas a pas, amb la seva lliçó:

# Què passa Peça Lliçó
1 El TLS acaba al balancejador, que reenvia amb X-Forwarded-Proto ALB / Ingress 08-03, 08-04
2 La petició entra a la cadena de filtres de servlet SecurityFilterChain 05-01
3 FiltreRastreig obre el span arrel i posa rastreId/spanId al MDC FiltreRastreig 03-06, 09-06
4 FiltreAutenticacioJwt valida la signatura HS256 i la caducitat, i construeix l'UsuariAutenticat ServeiJwt 05-04
5 AuthorizationFilter comprova que la ruta exigeix autenticació i que la té authorizeHttpRequests 05-02
6 El DispatcherServlet resol el handler i deserialitza el cos amb Jackson @RequestBody 03-02
7 Bean Validation comprova el DTO: @NotNull, @Positive, @MatriculaBicicleta IniciarLloguerRequest 03-04
8 El controlador delega en el servei; aquí no hi ha lògica LloguerController 03-03
9 El proxy del servei avalua @PreAuthorize abans d'entrar @EnableMethodSecurity 05-05
10 El mateix proxy obre la transacció: connexió del pool, autoCommit=false @Transactional 04-07
11 SELECT … FOR UPDATE reserva la millor bicicleta disponible i serialitza la cursa @Lock(PESSIMISTIC_WRITE) 04-07
12 SelectorTarifa tria la CalculadoraTarifa de l'usuari i calcula l'import TarifaEstandard i germanes 02-02
13 INSERT del lloguer i UPDATE de la bicicleta per dirty checking Spring Data JPA 04-05
14 El mapatge a LloguerResponse passa dins de la transacció LloguerMapper 03-05
15 COMMIT; la connexió torna al pool HikariCP 04-02
16 @TransactionalEventListener(AFTER_COMMIT) dispara l'esdeveniment ja confirmat LloguerIniciat 04-07
17 El cobrament surt en un altre fil, amb MDC i SecurityContext propagats @Async("executorCorreu") 07-03
18 La crida a la passarel·la porta traceparent, temps d'espera i tallacircuits RestClient + Resilience4j 07-06
19 El controlador respon 201 amb Location i X-Rastre-Id ResponseEntity 03-03
20 MetriquesCicloUrbana incrementa ciclourbana.lloguers.iniciats{tarifa,estacio} Micrometer 09-03
21 S'escriu una línia JSON a stdout amb el rastreId, i el span s'exporta per OTLP Logback + Tracing 09-05, 09-06

El que es veu en ajuntar-ho. Els passos 9 i 10 els fa el mateix proxy, i per això el parany de l'autoinvocació s'endú per davant la transacció i la comprovació de permisos alhora. El pas 14 no és un detall d'estil: amb open-in-view: false és l'única manera que la resposta es construeixi sense LazyInitializationException. I els passos 16 a 18 són la raó que la Marta rebi el seu 201 en desenes de mil·lisegons mentre el cobrament triga gairebé dos segons en un altre fil.

  1. Mapa del curs: què va construir cada mòdul

Mòdul Què es va construir Classes principals Fitxers del repositori
1 Introducció El projecte i el seu cicle de vida CicloUrbanaApplication, CarregadorEstacionsDemo pom.xml, mvnw, src/main/java, application.properties
2 Conceptes bàsics El contenidor, la configuració tipada i el sistema de tarifes CalculadoraTarifa, TarifaEstandard, TarifaEstudiant, SelectorTarifa, TarifesProperties, XarxaProperties comu/ConfiguracioComuna.java, application.yml
3 REST El contracte públic complet EstacioController, DTOs record, EstacioMapper, GestorGlobalExcepcions, FiltreRastreig, @MatriculaBicicleta */dto/, comu/, ConfiguracioOpenApi
4 Dades La persistència real Estacio, Bicicleta, Lloguer, EntitatAuditable, repositoris, PaginaResponse db/migration/V1…V9, application.yml (Hikari, JPA)
5 Seguretat Autenticació i autorització ConfiguracioSeguretat, ServeiJwt, FiltreAutenticacioJwt, UsuariAutenticat, SeguretatLloguers, AuditoriaSeguretat seguretat/, application-prod.yml
6 Proves La xarxa de seguretat TarifaEstandardTest, EstacioControllerTest, LloguerFluxCompletIT, ProvaIntegracioBase src/test/, JaCoCo i Failsafe al pom.xml
7 Operabilitat Iniciativa pròpia i empaquetament CaducadorLloguers, RecalculadorOcupacio, RevisorFlota, NotificadorLloguer, DecoradorMdc, ClientPassarelaPagaments Dockerfile, docker-compose.yml, V7__shedlock.sql
8 Desplegament El lliurament automàtic — .github/workflows/, helm/
9 Rendiment Veure i mesurar MetriquesCicloUrbana, FiltreComptadorConsultes, EstacioResum observabilitat/, carrega/, logback-spring.xml
10 Criteri La reflexió i les regles automàtiques ReglesArquitecturaTest, TarifaAmbRecarrecPerExces Spotless al pom.xml, docs/decisions/

  1. Decisions de disseny i les seves alternatives

Tota decisió d'arquitectura compra alguna cosa i en paga una altra. Aquestes són les sis principals de CicloUrbana, amb el que van costar.

Decisió Alternativa descartada Per què Contrapartida que assumim
Monòlit modular Microserveis Un equip, un domini acoblat, transaccions locals i una traça de pila que ho explica tot Escalar vol dir replicar l'aplicació sencera, encara que només els lloguers tinguin càrrega
JWT sense estat Sessió amb galeta Tres rèpliques sense sessió enganxosa ni magatzem compartit; el mòbil no gestiona galetes amb comoditat Un token no es pot revocar abans que caduqui; cal caducitat curta i refresc rotatori
Flyway ddl-auto: update L'esquema es revisa a la pull request, es versiona i es reprodueix igual als quatre entorns Cada canvi d'entitat exigeix escriure la migració a mà; i reanomenar costa tres desplegaments
MapStruct ModelMapper o mapatge manual unmappedTargetPolicy=ERROR converteix el camp oblidat en error de compilació, sense cost en execució Un processador d'anotacions més i codi generat que cal saber llegir
Caffeine + Redis Només Redis Caffeine respon en nanosegons i sense xarxa per al que tolera divergència de segons Dos nivells de memòria cau per invalidar; hi ha dades que poden diferir uns segons entre rèpliques
Testcontainers H2 en mode PostgreSQL Índexs parcials, tipus, funcions i bloqueigs es comporten de debò com en producció La suite d'integració triga minuts i necessita Docker a la màquina i a la canonada

I dues decisions menors que il·lustren la mateixa manera de pensar. open-in-view: false compra que cap consulta no es dispari durant la serialització, i paga amb l'obligació de mapejar a DTO dins de la transacció —cosa que, de passada, reforça una pràctica que volíem igualment—. La denegació per defecte compra que un oblit produeixi un 403 visible en lloc d'un endpoint obert, i paga amb el fet que cada ruta nova exigeixi una regla explícita.

Cap d'aquestes sis no és universalment correcta. El que les fa defensables és que es van prendre amb la contrapartida a la vista i van quedar escrites.

  1. El pom.xml final

Les dependències acumulades, agrupades pel mòdul que les va introduir. La columna de versió és la informació important: només porten versió pròpia les que el BOM de Spring Boot no gestiona, i són exactament les que cal revisar a mà a cada actualització.

Mòdul Dependències (org.springframework.boot: llevat d'indicació) Àmbit Versió
3 REST spring-boot-starter-web, spring-boot-starter-validation compile BOM
3 org.springdoc:springdoc-openapi-starter-webmvc-ui compile 2.6.0
3 org.mapstruct:mapstruct compile 1.6.3
4 Dades spring-boot-starter-data-jpa, org.flywaydb:flyway-core, flyway-database-postgresql compile BOM
4 org.postgresql:postgresql runtime BOM
5 Seguretat spring-boot-starter-security compile BOM
5 io.jsonwebtoken:jjwt-api (+ jjwt-impl i jjwt-jackson en runtime) mixt 0.12.6
7 Operació spring-boot-starter-actuator, spring-boot-starter-aop compile BOM
7 net.javacrumbs.shedlock:shedlock-spring + shedlock-provider-jdbc-template compile 5.16.0
7 io.github.resilience4j:resilience4j-spring-boot3 compile 2.2.0
9 Rendiment spring-boot-starter-cache, com.github.ben-manes.caffeine:caffeine, spring-boot-starter-data-redis compile BOM
9 io.micrometer:micrometer-registry-prometheus runtime BOM
9 io.micrometer:micrometer-tracing-bridge-otel, io.opentelemetry:opentelemetry-exporter-otlp compile BOM
9 net.logstash.logback:logstash-logback-encoder compile 8.0
6 Proves spring-boot-starter-test, spring-security-test, spring-boot-testcontainers, org.testcontainers:postgresql test BOM
10 Criteri com.tngtech.archunit:archunit-junit5 test 1.3.0
1 Desenvolupament spring-boot-devtools (runtime), spring-boot-configuration-processor optional BOM

I la part del pom.xml que de debò cal llegir amb atenció, perquè és la que trenca la construcció davant d'un problema concret:

<parent>                                       <!-- Mòdul 1: versions coherents -->
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.5</version>
</parent>

<groupId>com.ciclourbana</groupId>
<artifactId>ciclourbana</artifactId>
<version>2.4.0</version>
<properties><java.version>21</java.version></properties>

<build>
  <plugins>
    <plugin><groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId></plugin>

    <plugin>   <!-- MapStruct: el camp oblidat no compila (03-05) -->
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <annotationProcessorPaths>
          <path><groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>${mapstruct.version}</version></path>
        </annotationProcessorPaths>
        <compilerArgs>
          <arg>-Amapstruct.unmappedTargetPolicy=ERROR</arg>
          <arg>-Amapstruct.defaultComponentModel=spring</arg>
          <arg>-parameters</arg>   <!-- necessari per a #idUsuari a SpEL (05-05) -->
        </compilerArgs>
      </configuration>
    </plugin>

    <plugin>   <!-- Failsafe: les *IT a verify, no a test (06-01) -->
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-failsafe-plugin</artifactId>
      <executions><execution>
        <goals><goal>integration-test</goal><goal>verify</goal></goals>
      </execution></executions>
    </plugin>

    <plugin>   <!-- JaCoCo: mapa del vermell, no puntuació (06-01) -->
      <groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId>
      <version>0.8.12</version>
      <executions>
        <execution><goals><goal>prepare-agent</goal></goals></execution>
        <execution><phase>verify</phase><goals><goal>report</goal></goals></execution>
      </executions>
    </plugin>

    <plugin>   <!-- Spotless: l'estil no es discuteix (10-03) -->
      <groupId>com.diffplug.spotless</groupId>
      <artifactId>spotless-maven-plugin</artifactId><version>2.44.0</version>
      <configuration><java><palantirJavaFormat/><removeUnusedImports/></java></configuration>
      <executions><execution><phase>validate</phase>
        <goals><goal>check</goal></goals></execution></executions>
    </plugin>

    <plugin>   <!-- Dependency-Check: falla davant de severitat alta (05-05) -->
      <groupId>org.owasp</groupId><artifactId>dependency-check-maven</artifactId>
      <version>10.0.4</version>
      <configuration><failBuildOnCVSS>7</failBuildOnCVSS></configuration>
    </plugin>
  </plugins>
</build>

Els sis plugins no són adorn: cadascun trenca la construcció davant d'un problema concret —un camp sense mapejar, una prova d'integració fallida, codi sense formatar, una vulnerabilitat alta—. És la idea de fons de 10-01: moure la comprovació del cap d'una persona a la construcció.

  1. La configuració final

# application.yml — comú a tots els entorns
spring:
  application:
    name: ciclourbana                 # es converteix en service.name de les traces (09-06)
  threads:
    virtual:
      enabled: true                   # fils virtuals de Java 21 (07-03)
  jackson:
    default-property-inclusion: non_null
    deserialization: { fail-on-unknown-properties: false }   # tolerància del contracte (03-05)
  datasource:
    url: ${URL_BASE_DADES}            # sense valor per defecte: obligatori (10-01)
    username: ${USUARI_BASE_DADES}
    password: ${CONTRASENYA_BASE_DADES}
    hikari:
      pool-name: ciclourbana-pool
      maximum-pool-size: 10           # (nuclis × 2) + discos (09-01)
      minimum-idle: 10
      connection-timeout: 3000        # fallar ràpid, no encuar
      leak-detection-threshold: 20000
  jpa:
    open-in-view: false               # decisió estructural del mòdul 4
    hibernate: { ddl-auto: validate }  # l'esquema el governa Flyway
    properties:
      hibernate:
        jdbc.batch_size: 50
        order_inserts: true
        batch_versioned_data: true
  flyway: { enabled: true, locations: classpath:db/migration }
  cache:
    type: caffeine
    caffeine: { spec: "maximumSize=1000,expireAfterWrite=10m,recordStats" }
  task:
    execution:
      pool: { core-size: 8, max-size: 24, queue-capacity: 200 }
      thread-name-prefix: async-ciclo-
      shutdown: { await-termination: true, await-termination-period: 30s }
    scheduling:
      pool: { size: 4 }               # el planificador per defecte té UN fil (07-03)
      thread-name-prefix: tasca-ciclo-
  lifecycle:
    timeout-per-shutdown-phase: 40s   # més gran que els await-termination-period

server:
  shutdown: graceful                  # aturada ordenada (01-05, 07-03)
  forward-headers-strategy: framework # el TLS acaba al balancejador (05-05)
  error: { include-stacktrace: never, include-message: never }
  compression:
    enabled: true
    mime-types: application/json,application/problem+json
    min-response-size: 1KB

management:
  server:
    port: 8081                        # port de gestió separat (07-01)
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus,loggers,scheduledtasks
  endpoint:
    health:
      probes: { enabled: true }       # /health/liveness i /health/readiness
      show-details: when_authorized
  tracing:
    sampling: { probability: 1.0 }    # el mostreig el decideix el Collector, per cua (09-06)
  otlp:
    tracing:
      endpoint: ${OTLP_ENDPOINT:http://otel-collector:4318/v1/traces}
  metrics:
    tags: { aplicacio: ciclourbana }

ciclourbana:                          # propietats pròpies, validades (02-05)
  ciutat: Ribalta
  tarifes:
    desbloqueig: 0.50
    preu-minut: 0.12
    preu-minut-estudiant: 0.08
  xarxa:
    capacitat-minima: 8
    llindar-bateria: 20
    durada-maxima-lloguer: PT2H
    estacions-destacades: [Plaça Major, Universitat]
  lloguers:
    caducador:
      interval: PT10M
  jwt:
    # el secret arriba de l'entorn; vegeu 05-04
    secret: ${JWT_SECRET}             # obligatori, mai versionat
    expiracio: PT15M
    expiracio-refresc: P7D

I les diferències per entorn, que és l'única cosa que canvia:

Propietat dev test pre prod
Base de dades Docker local Testcontainers RDS de preproducció RDS amb rèplica
flyway.locations + db/demo db/migration db/migration db/migration
logging.level.com.ciclourbana DEBUG INFO INFO INFO
Swagger UI Actiu Actiu Actiu springdoc.api-docs.enabled: false
CORS http://localhost:5173 — https://pre.ribalta.example https://panel.ribalta.example
Memòria cau Caffeine Deshabilitada Redis Redis
tracing.sampling 1.0 local 0.0 1.0 → Collector 1.0 → Collector amb cua
Actuator exposat "*" health Amb ADMIN health,info,prometheus

  1. Posar en marxa el projecte des de zero

# 1. Clonar i preparar els secrets locals
git clone https://github.com/ajuntament-ribalta/ciclourbana.git
cd ciclourbana
cp .env.exemple .env          # emplenar CONTRASENYA_BASE_DADES i JWT_SECRET
openssl rand -base64 48       # generar un secret JWT de 256+ bits

# 2. Aixecar la infraestructura (PostgreSQL 16 + Redis)
docker compose up -d
docker compose ps             # esperar que el healthcheck marqui "healthy"

# 3. Construcció completa: format, unitàries, *IT amb Testcontainers, cobertura
./mvnw verify
open target/site/jacoco/index.html

# 4. Arrencar en desenvolupament
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev

# 5. Comprovar que és viva i amb l'esquema al dia
curl -s localhost:8081/actuator/health/readiness | jq
curl -s localhost:8081/actuator/info | jq '.build.version'    # 2.4.0

# 6. (Opcional) La pila d'observabilitat completa
docker compose -f docker-compose.observabilitat.yml up -d
# Grafana a http://localhost:3000 · Prometheus 9090 · Tempo 3200

El pas 3 mereix un comentari: ./mvnw verify aixeca un PostgreSQL efímer amb Testcontainers i aplica les nou migracions a sobre abans d'executar les *IT. Si aquesta ordre passa a la teva màquina, el projecte està sa de dalt a baix.

  1. Recorregut d'extrem a extrem amb peticions .http

### peticions/ciclourbana.http
@host = http://localhost:8080/api/v1

### 1. Registre d'una ciutadana de Ribalta
POST {{host}}/auth/registre
Content-Type: application/json

{ "nom": "Marta Serra", "correu": "[email protected]",
  "contrasenya": "Bicicleta-2026!", "tipusTarifa": "ESTANDARD" }

> {% client.test("201 i sense dades sensibles", () => {
     client.assert(response.status === 201);
     client.assert(response.body.contrasenyaHash === undefined); }); %}

### 2. Login: retorna el token d'accés i el de refresc
# @name login
POST {{host}}/auth/login
Content-Type: application/json

{ "correu": "[email protected]", "contrasenya": "Bicicleta-2026!" }

> {% client.global.set("token", response.body.tokenAcces); %}

### 3. Estacions amb bicicletes disponibles (públic, paginat)
GET {{host}}/estacions?pagina=0&mida=20
Authorization: Bearer {{token}}

### 4. Detall de «Plaça Major», amb les seves bicicletes ancorades
GET {{host}}/estacions/1
Authorization: Bearer {{token}}

### 5. Iniciar el lloguer: 201 amb Location i X-Rastre-Id
# @name lloguer
POST {{host}}/lloguers
Authorization: Bearer {{token}}
Content-Type: application/json

{ "estacioOrigenId": 1, "matricula": "RB-0142" }

> {% client.global.set("lloguerId", response.body.id);
     client.global.set("rastre", response.headers.valueOf("X-Rastre-Id")); %}

### 6. Finalitzar a «Universitat»: calcula l'import i allibera la bicicleta
PATCH {{host}}/lloguers/{{lloguerId}}/finalitzar
Authorization: Bearer {{token}}
Content-Type: application/json

{ "estacioDestiId": 4 }

### 7. La factura del lloguer
GET {{host}}/lloguers/{{lloguerId}}
Authorization: Bearer {{token}}

### 8. Comprovació de seguretat: el lloguer d'un altre ciutadà dona 403
GET {{host}}/lloguers/9999
Authorization: Bearer {{token}}

I les tres comprovacions que tanquen el recorregut, que són les que demostren que l'observabilitat del mòdul 9 funciona de debò:

# La mètrica de negoci s'ha mogut
curl -s localhost:8081/actuator/metrics/ciclourbana.lloguers.iniciats | jq '.measurements'

# El log de la petició és consultable pel seu rastreId (Loki, 09-05)
# {app="ciclourbana"} | json | rastreId = "<el X-Rastre-Id del pas 5>"

# I la traça completa és a Tempo, amb el span del cobrament (09-06)
curl -s "http://localhost:3200/api/traces/<rastreId>" | jq '.batches | length'

Si els tres retornen dades, les tres senyals estan connectades: la mètrica detecta, la traça localitza i el log explica.

  1. Llista de comprovació final de posada en producció

# Comprovació Verificació objectiva
1 Zero secrets al repositori i al seu historial Detector de secrets amb --log-opts="--all"; rotar el que es trobi
2 El perfil prod s'activa de debò Buscar al log d'arrencada The following 1 profile is active: "prod"
3 ./mvnw verify en verd, amb les *IT incloses La canonada de ci.yml, no la màquina de ningú
4 Migracions aplicades i compatibles cap enrere flyway:info i la prova de l'apartat 5 amb la versió anterior corrent
5 Denegació per defecte i regles per dada Prova automatitzada de rutes sense token i d'identificadors creuats
6 Swagger, consola i Actuator tancats curl a /swagger-ui.html, /h2-console i /actuator/env: 404 o 401
7 HTTPS obligatori amb HSTS i certificat vàlid curl -I sobre HTTP: ha de rebutjar o redirigir
8 Sondes liveness i readiness diferenciades El manifest: livenessProbe no consulta la base de dades
9 Recursos i HPA dimensionats kubectl describe hpa i la prova de càrrega de k6 contra pre
10 Logs en JSON amb rastreId, sense credencials Buscar eyJ, Bearer i un correu conegut en un flux complet: zero
11 Mètriques i alertes actives Un tauler amb trànsit real i una alerta que es dispara en provocar-la
12 Revertir és una ordre cronometrada Executar-la de debò a pre i anotar el temps
13 Còpies de seguretat de PostgreSQL restaurades Restaurar una còpia en un entorn a part; una còpia no provada no existeix
14 Sense vulnerabilitats altes dependency-check amb failBuildOnCVSS=7 en verd
15 Revisió de seguretat professional Informe signat d'auditoria i prova de penetració

El punt 13 no va aparèixer en cap lliçó i és dels que surten més cars: una còpia de seguretat que mai no s'ha restaurat no és una còpia de seguretat, és una esperança. I el 15 repeteix l'advertiment del mòdul 5: tot el que s'ha construït en aquest curs és un punt de partida didàctic i necessita revisió professional abans d'exposar-se a Internet.

  1. Cap a on continuar amb aquest projecte

CicloUrbana està acabada com a material de curs, no com a producte. Aquestes set extensions estan ordenades per dificultat i cadascuna combina lliçons que ja coneixes.

# Extensió Dificultat Què cal combinar
1 Informes amb exportació a CSV de l'històric de lloguers Baixa Consultes i projeccions 04-06, paginació per keyset 09-01, resposta en flux i @Async 07-03
2 Panell d'operari amb la vista interna de la flota Baixa DTOs separats 03-05, rol OPERARI i @PreAuthorize 05-05, proves de seguretat 06-04
3 Integració amb un mapa i cerca per proximitat Mitjana RestClient amb tallacircuits 07-06, memòria cau de geocodificació 09-02, índex geoespacial 04-08
4 Reserva anticipada de bicicleta, amb caducitat als 10 minuts Mitjana Bloqueig pessimista i estats 04-07, tasca amb ShedLock 07-03, nova migració i invariants al domini 10-03
5 Notificacions push al mòbil Mitjana Esdeveniments AFTER_COMMIT 04-07, executor propi i mampara 07-03, tolerància a fallades 07-06
6 Tarifes dinàmiques per demanda en hora punta Alta Una altra CalculadoraTarifa i decoradors 02-02, mètriques d'ocupació 09-03, @ConfigurationProperties 02-05, proves parametritzades 06-02
7 Extreure la facturació a un servei propi Molt alta El mòdul 7 sencer, traçabilitat prèvia 09-06, patró outbox i saga, contracte versionat 03-01

Sobre la número 7, que és la que més gent vol fer primer: és l'última de la llista per una raó. Repartir el sistema en dos processos converteix una transacció local en una saga amb compensacions, una traça de pila en una investigació entre dos desplegaments, i una crida a mètode en una crida de xarxa que pot fallar. Fes-la només quan tinguis traçabilitat distribuïda funcionant i una raó concreta —escalat o equip independent—, no com a exercici d'estil.

I una recomanació sobre el mètode: per a qualsevol de les set, comença per escriure la prova del comportament que vols, segueix per la migració si toca l'esquema, i deixa el controlador per al final. És l'ordre que menys codi llença a les escombraries.

Errors Comuns i Consells

Creure que el diagrama és l'arquitectura. El diagrama de l'apartat 1 és una vista; l'arquitectura real és el que fan les dependències del codi. Per això existeixen les regles d'ArchUnit: són l'únic diagrama que no pot mentir.

Copiar el pom.xml sencer a un projecte nou. Porta Redis, ShedLock, Resilience4j i Testcontainers a un projecte que potser no els necessita, i cada dependència és superfície de manteniment i de vulnerabilitats. Comença per web, data-jpa i test, i afegeix quan alguna cosa faci mal.

Aixecar l'observabilitat sencera en local sempre. Prometheus, Grafana, Loki, Tempo i el Collector consumeixen memòria i temps d'arrencada. En el dia a dia n'hi ha prou amb l'aplicació i PostgreSQL; la pila completa s'aixeca quan s'està treballant en observabilitat.

Oblidar que el .env no és al repositori. És el primer que trenca qui clona el projecte per primera vegada. Un .env.exemple versionat amb les claus i sense els valors estalvia aquesta mitja hora a cada persona nova.

Consell: recorre l'apartat 3 amb un depurador. Posar un punt d'interrupció a LloguerService.iniciar i pujar per la pila de crides fins al filtre és l'exercici que més consolida tot el curs: es veuen, en una sola pantalla, el proxy, la transacció, el SecurityContext i el MDC.

Consell: guarda el registre de decisions. La taula de l'apartat 5 —decisió, alternativa, motiu, contrapartida— és el document que més s'agraeix d'aquí a un any, i l'únic que respon a «per què està fet així?».

Consell: mesura abans d'estendre. Qualsevol de les set extensions de l'apartat 11 canvia el perfil de càrrega. Guarda la línia base de k6 d'avui: serà la referència per saber si l'extensió ha costat alguna cosa.

Exercicis

Exercici 1: seguir el fil d'una fallada

En producció, a les 09:12, l'app mòbil comença a rebre 500 a POST /api/v1/lloguers. El cos de l'error és un ProblemDetail amb "codi": "ERROR_INTERN" i un rastreId. Descriu, pas a pas i amb les eines concretes del projecte, com investigaries l'incident des d'aquest rastreId fins a la causa arrel, indicant quina peça de quin mòdul faries servir a cada pas i què descartaries amb cadascuna.

Exercici 2: dissenyar la reserva anticipada

Dissenya l'extensió número 4 de l'apartat 11: un ciutadà pot reservar una bicicleta concreta durant 10 minuts abans de recollir-la; passat aquest temps, la reserva caduca i la bicicleta torna a estar disponible. Especifica el canvi al model, la migració, el contracte de l'API, el control de concurrència, la tasca de caducitat, les regles de seguretat, les mètriques i les proves que escriuries.

Exercici 3: revisió d'arquitectura

L'ajuntament d'una ciutat veïna vol reutilitzar CicloUrbana. Els seus números: 40 estacions (davant de 4), 300.000 lloguers al mes, dos equips de desenvolupament i un requisit nou —integració en temps real amb el sistema de transport públic—. Analitza quines decisions de l'apartat 5 continuarien sent correctes, quines caldria revisar i en quin ordre abordaries l'adaptació.

Solucions

Solució 1

Pas 1 — de l'alerta al quadre de comandament (09-04). Abans de tocar el rastreId, mirar la fila RED del tauler: és un pic d'errors o un degoteig? va començar amb el desplegament de les 09:05? Amb això es distingeix una fallada sistemàtica d'una de puntual, i es decideix si cal revertir ja.

Pas 2 — el log d'aquella petició exacta (09-05). A Loki: {app="ciclourbana", entorn="prod"} | json | rastreId = "...". Retorna la història completa de la petició, inclosa la traça de pila que no se li va enviar al client. La majoria dels incidents es tanquen aquí, amb el nom de l'excepció i la línia.

Pas 3 — la cascada de spans (09-06). A Tempo, la mateixa traça. Aquí es respon una cosa que el log no diu: on se'n va anar el temps i què va fallar. Una barra vermella al span de la passarel·la descarta que el problema sigui nostre; vint spans select idèntics delaten un N+1 nou; un forat de 900 ms sense instrumentar apunta a espera de connexió o a una pausa de GC.

Pas 4 — descartar recursos (09-03). hikaricp.connections.pending, jvm.gc.pause i resilience4j.circuitbreaker.state a l'instant de la fallada. Si pending és a 60, el problema no és la lògica: alguna cosa reté connexions —una crida HTTP dins d'una transacció és el sospitós número u—.

Pas 5 — reproduir (06-05). Amb la causa identificada, escriure la prova que la reprodueix abans d'arreglar res. Si la fallada depèn de dades reals, una *IT amb Testcontainers i l'estat exacte que la provoca.

Pas 6 — decidir (08-05). Si la fallada va arribar amb el desplegament de les 09:05, revertir primer i diagnosticar després: el temps de restauració és una mètrica DORA i la reversió és una ordre. Si és un problema extern, la decisió és de negoci —degradar, obrir el tallacircuits, esperar—, com en el cas pràctic de 09-06.

El que l'exercici ensenya: el recorregut va del general al particular i del barat al car. La mètrica costa una llambregada, la traça un clic, el log una consulta, i el depurador mig matí. Començar pel depurador és l'error més comú.

Solució 2

Model. EstatBicicleta guanya el valor RESERVADA. Apareix l'entitat Reserva(id, usuari, bicicleta, estacio, creada, expira, estat) amb EstatReserva { ACTIVA, CONSUMIDA, CADUCADA, CANCELLADA }. La reserva és un agregat propi i no un camp de Bicicleta, perquè té cicle de vida, històric i regles pròpies.

Migració V10__reserves.sql. Taula amb seqüència (allocationSize = 50, mai IDENTITY, pels lots de 09-01), claus foranes a usuaris, bicicletes i estacions, columnes d'auditoria, CHECK sobre l'estat, índex (expira) WHERE estat = 'ACTIVA' —parcial, unes desenes de files davant de milions— i un índex únic parcial que impedeix dues reserves actives del mateix usuari, la mateixa tècnica de l'uk_lloguers_usuari_en_curs. L'enum de la columna estat de bicicletes amplia el seu CHECK: canvi compatible cap enrere.

Contracte. POST /api/v1/reserves amb { "bicicletaId": 12 } retorna 201 amb Location i ReservaResponse(id, matricula, estacio, expira, segonsRestants). DELETE /api/v1/reserves/{id} cancel·la (204). I POST /api/v1/lloguers accepta opcionalment reservaId: si ve, consumeix la reserva en lloc de cercar bicicleta.

Concurrència. És el punt delicat. SELECT … FOR UPDATE sobre la bicicleta amb timeout de 3 s, comprovació que continua DISPONIBLE, i canvi a RESERVADA a la mateixa transacció. Sense bloqueig, dos ciutadans reserven la mateixa bicicleta i un s'emporta un 409 innecessari quan el sistema li podria haver ofert una altra.

Caducitat. CaducadorReserves, germà de CaducadorLloguers: @Scheduled(fixedDelayString = "${ciclourbana.reserves.caducador.interval:PT30S}") amb @SchedulerLock —tres rèpliques, una execució—, lògica en un mètode públic invocable per poder provar-la, Clock injectat i idempotència: marcar com a caducada una reserva ja caducada no canvia res.

Seguretat. @PreAuthorize("isAuthenticated()") per crear; @PreAuthorize("hasAnyRole('OPERARI','ADMIN') or @seguretatReserves.esPropietari(#idReserva, principal)") per cancel·lar i consultar, amb un bean nou idèntic en forma a SeguretatLloguers. El llistat filtra a la consulta, mai amb @PostFilter.

Mètriques. Un Counter ciclourbana.reserves.creades{estacio} i un altre ciclourbana.reserves.caducades, amb l'etiqueta d'estació —quatre valors— i mai l'identificador d'usuari. La taxa de caducitat és l'indicador de negoci interessant: si és alta, la finestra de 10 minuts és massa curta o massa llarga.

Proves. Unitària de Reserva.caducar() amb Clock.fixed i del cas límit dels 10 minuts exactes; @DataJpaTest de l'índex únic parcial —dues reserves actives del mateix usuari han de violar la restricció—; llesca de seguretat amb dos ciutadans creuant identificadors; i una *IT del flux complet reservar → llogar → finalitzar, més una altra de reservar → esperar → comprovar que ha caducat, amb el rellotge controlat.

La decisió de disseny que cal saber defensar: la reserva és un agregat propi. Modelar-la com dos camps a Bicicleta sembla més barat i fa impossible l'històric, complica la caducitat i fica estat transitori en una entitat que hauria de ser estable.

Solució 3

El que continua sent correcte sense discussió. Flyway, DTOs, denegació per defecte, @Transactional al servei, Testcontainers, logs estructurats i observabilitat: són decisions el benefici de les quals creix amb la mida, no decreix. MapStruct passa d'opcional a clarament rendible, perquè el nombre de DTOs es multiplica.

El que cal revisar, amb el seu motiu.

Decisió Estat amb els números nous Què fer
Monòlit modular Continua sent correcte, i per poc 300.000 lloguers al mes són ~7 per minut de mitjana: ridícul per a un monòlit. El que empeny no és la càrrega sinó dos equips; la resposta raonable és Spring Modulith i fronteres internes més estrictes abans que dividir
Caffeine local Insuficient Amb més rèpliques, 40 estacions i dades que canvien, la divergència entre memòries cau locals es nota. Redis com a nivell compartit, Caffeine al davant per al que és immutable
Pool de 10 connexions A recalcular Depèn del servidor nou, no de l'anterior: fórmula, prova de càrrega i pending com a senyal
JWT de 15 minuts Correcte, amb matisos Amb dos equips i més clients, cal gestió de claus i rotació; i probablement un proveïdor d'identitat en lloc de signar nosaltres
estacions sense índex geoespacial Insuficient Amb 40 estacions i cerca per proximitat, PostGIS o almenys un índex sobre les coordenades
Integració amb transport públic Decisió nova És el primer cas real de missatgeria: horaris en temps real no encaixen en peticions síncrones. Kafka o similar, amb el patró outbox

L'ordre de l'adaptació, que és el que l'exercici pregunta de debò:

  1. Mesurar amb les dades noves abans de tocar res. Carregar 40 estacions i 300.000 lloguers a pre i executar l'escenari de k6. Gairebé totes les decisions anteriors es resolen amb dades, no amb opinions.
  2. Índexs i consultes. El que es degrada amb el volum es degrada primer, i és el més barat d'arreglar.
  3. Fronteres de mòdul abans que equips. Amb dos equips, el conflicte no és tècnic sinó de propietat del codi: regles d'ArchUnit per mòdul i fronteres explícites abans de repartir la feina.
  4. Memòria cau distribuïda i pool redimensionat, amb la prova de càrrega com a jutge.
  5. La integració de transport públic, l'última, perquè introdueix una tecnologia nova —missatgeria— i convé fer-ho sobre una base ja estable i mesurada.

El que no faria: dividir en microserveis des del principi «perquè ara és més gran». Quaranta estacions i set lloguers per minut no són un problema d'escala: són un problema d'organització, i aquest es resol amb fronteres internes abans que amb processos separats.

Conclusió

CicloUrbana està completa i, per primera vegada, l'has vista sencera. Tens l'arquitectura final en un diagrama que es llegeix: un sol desplegable amb mòduls interns, dependències que apunten sempre cap endins, l'observabilitat com a lateral i no com a capa, i una única dependència capaç de tombar el servei —PostgreSQL—, cosa que explica per què la sonda readiness la consulta i la de liveness no. Tens l'estructura real del repositori, amb els paquets per funcionalitat, l'arbre de proves com a mirall del de producció, la separació *Test/*IT i tot el que envolta el codi: Dockerfile, docker-compose, helm/, .github/workflows, observabilitat/ i carrega/.

I tens el viatge d'una petició explicat en vint-i-un passos, del balancejador a la mètrica, amb la lliçó de cadascun. Aquest recorregut és el millor resum del curs perquè hi apareix tot: el filtre que obre el span, el JWT que es valida, el binding i la validació del DTO, el proxy que avalua el permís i obre la transacció al mateix punt —d'aquí que l'autoinvocació se'ls endugui tots dos per davant—, el bloqueig pessimista sobre l'última bicicleta de «Plaça Major», el mapatge dins de la transacció que fa possible open-in-view: false, l'AFTER_COMMIT que treu el cobrament del fil HTTP i la mètrica de negoci que tanca el cicle.

Tens també el mapa de què va construir cada mòdul, el pom.xml final on es veu quines versions gestiona el BOM i quines cal revisar a mà, la configuració consolidada amb la taula de l'única cosa que canvia entre entorns, les ordres per posar el projecte en marxa des de zero i la col·lecció .http que recorre registre, login, consulta, lloguer, factura i la comprovació que un ciutadà no pot llegir el lloguer d'un altre —seguida de les tres consultes que demostren que la mètrica, el log i la traça estan units pel mateix rastreId—.

I tens les dues peces que separen un projecte de curs d'un producte: la taula de decisions amb les seves contrapartides, que és el document que respon d'aquí a un any a «per què està fet així?», i la llista de comprovació final amb els seus quinze punts, inclòs el que cap lliçó no havia esmentat —una còpia de seguretat que mai no s'ha restaurat no és una còpia de seguretat— i el que es repeteix des del mòdul 5: això és un punt de partida didàctic i necessita revisió professional abans d'exposar-se a Internet.

El projecte no s'acaba aquí, i aquesta és la idea. Les set extensions de l'apartat 11 estan ordenades per dificultat i cadascuna et diu quines lliçons combinar, des de l'informe amb exportació fins a extreure la facturació a un servei propi —l'última de la llista, i amb motiu—. Construir qualsevol d'elles sobre aquesta base és la millor manera de convertir el que has après en alguna cosa teva.

Queda una última lliçó, i és l'única del curs que no parla de CicloUrbana. Perquè saber Spring Boot no és haver acabat un curs: és haver començat a poder llegir la documentació amb criteri, distingir un recurs actualitzat d'un que confon, planificar una actualització de versió major sense por i triar què aprendre després entre vint camins possibles. Recursos per a Aprenentatge Addicional s'ocupa d'això: la documentació oficial i com llegir-la, el codi font com la millor font, com mantenir-se al dia amb el cicle de publicació de Spring, l'ecosistema Java, els llibres que de debò valen la pena, les certificacions amb una valoració honesta, la comunitat, la pràctica deliberada, els temes naturals per al pas següent i un full de ruta raonat per als propers sis mesos.

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