La lliçó anterior va tancar amb una pregunta diferent de totes les que havíem fet fins ara: CicloUrbana està construïda, provada, desplegada i observada, però està ben feta? Durant nou mòduls hem pres decisions sense aturar-nos gaire a justificar-les com a categoria: vam posar @Transactional al servei, vam fer els DTOs record, vam deixar open-in-view: false, vam denegar per defecte a la cadena de filtres. Cadascuna tenia la seva raó al seu moment, escampada a la lliçó on va aparèixer.

Aquesta lliçó les recull i les ordena. No com una llista de manaments —això seria inútil i probablement perjudicial—, sinó com un catàleg de decisions justificades: per a cada pràctica, quin problema resol, com es veu al codi de la xarxa de Ribalta, on la vam estudiar i —això és el que separa un professional d'algú que copia receptes— en quines circumstàncies té sentit no aplicar-la. Al final tindràs una llista de comprovació de trenta-vuit punts que pots endur-te a qualsevol projecte Spring Boot i fer-la servir el dia abans d'un desplegament.

Contingut

  1. Una pràctica no és un dogma
  2. Estructura i disseny
  3. Configuració
  4. Injecció i beans
  5. L'API
  6. Dades i persistència
  7. Seguretat
  8. Proves
  9. Operació
  10. La llista de comprovació d'una aplicació llesta per a producció
  11. Quan trencar la regla
  12. Errors Comuns i Consells
  13. Exercicis

  1. Una pràctica no és un dogma

Abans del catàleg, un avís sobre com llegir-lo.

Una «bona pràctica» és la resposta que sol ser correcta a un problema recurrent, en un context determinat. Les tres paraules importen. Si s'oblida el context, la pràctica es converteix en superstició: gent que crea una interfície per cada servei perquè «així es fa», sense que existeixi ni hagi d'existir una segona implementació. Si s'oblida el problema, es perd la capacitat de saber quan la pràctica ja no aporta res.

Per això cada apartat d'aquesta lliçó té sempre la mateixa forma:

Element Què respon
La pràctica Què es fa
El perquè Quin problema concret evita
A CicloUrbana On es veu al codi de Ribalta
On es va estudiar La lliçó que ho desenvolupa

I hi ha un apartat 11 dedicat exclusivament al contrari: quan trencar la regla amb criteri. Una regla que no pots justificar no és una regla que domines, és una que has memoritzat.

Un darrer marc general abans de començar. Gairebé totes les pràctiques d'aquesta lliçó es deriven de tres principis de fons:

flowchart TB
    P1["Fer explícit l'implícit<br/>DTOs, propietats validades,<br/>migracions versionades"]
    P2["Fallar aviat i sorollosament<br/>arrencada, compilació, proves<br/>abans que producció"]
    P3["Separar el que canvia<br/>per raons diferents<br/>domini / contracte / infraestructura"]
    P1 --- P2 --- P3

Si algun cop dubtes davant d'una decisió que aquest catàleg no cobreix, pregunta't quin dels tres principis es respecta millor amb cada opció. Sol ser suficient.

  1. Estructura i disseny

2.1. Organitza per funcionalitat, no per capa tècnica

La pràctica. Els paquets de primer nivell són àrees del negoci (estacions, bicicletes, lloguers, usuaris), no tipus tècnics (controller, service, repository, model).

El perquè. Un canvi real gairebé mai no és «tocar tots els controladors»: és «afegir un camp a les estacions», i això toca controlador, servei, repositori, DTO i mapejador. Amb paquets per capa, aquest canvi es reparteix per cinc carpetes llunyanes; amb paquets per funcionalitat, cap en una. A més, els paquets per funcionalitat permeten fer servir la visibilitat de paquet de Java com una frontera real, cosa que els paquets per capa fan impossible: si tots els serveis són junts, tots són visibles entre ells.

❌ Per capa                        ✅ Per funcionalitat
com.ciclourbana                    com.ciclourbana
├── controller                     ├── CicloUrbanaApplication.java
│   ├── EstacioController          ├── estacions
│   ├── BicicletaController        │   ├── Estacio.java
│   └── LloguerController          │   ├── EstacioRepositori.java
├── service                        │   ├── EstacioService.java
│   ├── EstacioService             │   ├── EstacioController.java
│   └── ...                        │   ├── EstacioMapper.java
├── repository                     │   └── dto/
└── model                          ├── bicicletes
                                   ├── lloguers
                                   ├── usuaris
                                   ├── seguretat
                                   └── comu

A CicloUrbana. És l'estructura que arrosseguem des d'Entendre l'Estructura del Projecte: com.ciclourbana.lloguers conté Lloguer, LloguerRepositori, LloguerService, LloguerController, LloguerMapper, les tarifes i el subpaquet dto. com.ciclourbana.comu guarda el transversal —PaginaResponse, EntitatAuditable, el Clock, FiltreRastreig, GestorGlobalExcepcions—.

2.2. El monòlit modular per defecte

La pràctica. Un sol desplegable amb fronteres internes fortes, fins que existeixi una raó concreta per partir-lo.

El perquè. A Spring Boot i Microserveis ho vam veure amb noms: dividir el sistema canvia crides a mètodes per crides de xarxa, transaccions locals per sagues, una traça de pila per una investigació entre tres equips. Tot això són costos reals que només es paguen bé quan compren alguna cosa —escalat independent, desplegament independent, equips independents— que el projecte necessita de debò.

A CicloUrbana. Els quatre paquets de negoci són mòduls amb el seu propi servei com a façana. LloguerService no consulta EstacioRepositori directament; passa per EstacioService o reacciona a esdeveniments. Aquesta disciplina és la que fa que extreure la facturació a un servei propi sigui, el dia que calgui, una feina de dies i no de mesos.

2.3. La regla de dependència entre capes

La pràctica. El flux de dependències és unidireccional: Controlador → Servei → Repositori. Mai a l'inrevés, i mai saltant-se el pas intermedi.

El perquè. Un repositori que crida un servei crea cicles —els que feien fallar l'arrencada a Injecció de Dependències— i fa impossible raonar sobre l'ordre de les coses. Un controlador que crida el repositori se salta la transacció, les regles de negoci i les comprovacions de seguretat de mètode de Seguretat a Nivell de Mètode, que viuen precisament al servei.

flowchart LR
    C["EstacioController<br/>HTTP, DTOs, codis"] --> S["EstacioService<br/>@Transactional, @PreAuthorize,<br/>regles de negoci"]
    S --> R["EstacioRepositori<br/>consultes"]
    R --> BD[(PostgreSQL)]
    C -.->|"❌ mai"| R
    R -.->|"❌ mai"| S

A CicloUrbana. EstacioController.crear crida estacioService.crear(...) i mai estacioRepositori.save(...). A Consells per Escriure Codi Net veurem com convertir aquesta regla en una prova automàtica amb ArchUnit, de manera que deixar de complir-la posi la construcció en vermell.

2.4. El domini no depèn del framework

La pràctica. Les classes que expressen regles de negoci no importen org.springframework.* ni jakarta.servlet.*.

El perquè. El domini és la part del codi que més viu i menys canvia; el framework és la que més canvia. Si CalculadoraTarifa importés HttpServletRequest, la regla de les tarifes de Ribalta quedaria lligada al fet que la petició sigui HTTP. I hi ha un benefici immediat: una classe sense framework es prova en un mil·lisegon, que és la raó que TarifaEstandardTest d'Introducció a les Proves no aixequi cap context.

// ✅ Domini pur: s'instancia amb new i es prova sense Spring
public interface CalculadoraTarifa {
    BigDecimal calcular(Duration durada);
    default String nom() { return getClass().getSimpleName(); }
}

L'anotació @Component sobre TarifaEstandard és l'excepció tolerada i conscient: és una metadada que no canvia el comportament de la classe i no impedeix instanciar-la amb new en una prova.

  1. Configuració

3.1. @ConfigurationProperties validades, no @Value escampat

La pràctica. Els grups de propietats s'enllacen a un record validat; @Value queda per a casos veritablement solts.

El perquè. Cinc @Value repartits per quatre classes són cinc llocs on un error d'escriptura no dona error de compilació, cinc valors sense validar i cap documentació de què configura l'aplicació. Un record amb prefix és un contracte: surt a /actuator/configprops, l'autocompleta l'IDE i falla en arrencar si alguna cosa no quadra.

// ❌ Així no
@Service
public class TarifaEstandard {
    @Value("${ciclourbana.tarifes.desbloqueig}") private BigDecimal desbloqueig;
    @Value("${ciclourbana.tarifes.preu-minut}") private BigDecimal preuMinut;
}
// ✅ Així sí
@Validated
@ConfigurationProperties(prefix = "ciclourbana.tarifes")
public record TarifesProperties(
        @NotNull @DecimalMin("0.00") BigDecimal desbloqueig,
        @NotNull @DecimalMin("0.01") BigDecimal preuMinut,
        @NotNull @DecimalMin("0.00") BigDecimal preuMinutEstudiant) {}

On es va estudiar. Propietats de Spring Boot, amb TarifesProperties i XarxaProperties.

3.2. Un artefacte per a tots els entorns

La pràctica. El mateix JAR i la mateixa imatge viatgen de dev a pre i a prod. El que canvia és l'entorn, mai el binari.

El perquè. Recompilar per entorn vol dir desplegar en producció un artefacte que ningú no va provar. És el segon dels dotze factors i la raó que els perfils existeixin.

A CicloUrbana. application.yml amb el que és comú i application-dev/test/pre/prod.yml amb les diferències, activats per SPRING_PROFILES_ACTIVE (Perfils de Spring Boot). El perfil mai no es cou dins de la imatge Docker.

3.3. Els secrets fora del repositori, sempre

La pràctica. Contrasenya de PostgreSQL, secret de signatura del JWT i clau de la passarel·la arriben per variable d'entorn o gestor de secrets. Mai per un fitxer versionat, ni «temporalment».

El perquè. Un secret que entra a l'historial de Git hi continua sent encara que el següent commit l'esborri. Esborrar-lo no n'hi ha prou: cal rotar-lo.

# application-prod.yml — versionat, sense ni un sol valor secret
spring:
  datasource:
    url: ${URL_BASE_DADES}
    username: ${USUARI_BASE_DADES}
    password: ${CONTRASENYA_BASE_DADES}
ciclourbana:
  jwt:
    # el secret de signatura arriba de l'entorn, mai del fitxer
    secret: ${JWT_SECRET}

3.4. Sense valor per defecte per al que és obligatori

La pràctica. Una propietat que ha de venir de l'entorn no porta valor per defecte. Si falta, l'aplicació no arrenca.

El perquè. És el principi de fallar aviat aplicat a la configuració. Un ${JWT_SECRET:canviam} arrenca perfectament en producció i signa tokens amb un secret públic; sense valor per defecte, el desplegament falla a l'arrencada, la sonda de disponibilitat no passa i el desplegament es reverteix sol.

Tipus de propietat Valor per defecte? Exemple
Secret Mai ${JWT_SECRET}
Adreça d'un servei extern Només la de desenvolupament local ${OTLP_ENDPOINT:http://localhost:4318/v1/traces}
Paràmetre de negoci Sí, el valor de Ribalta capacitat-minima: 8
Cadència d'una tasca Sí, l'interval raonable ${...caducador.interval:PT10M}

  1. Injecció i beans

4.1. Injecció per constructor, camps final, sense @Autowired

La pràctica. Totes les dependències entren pel constructor, es guarden en camps final i no s'anota @Autowired quan hi ha un sol constructor.

El perquè. Dels cinc arguments d'Injecció de Dependències, dos són decisius: la classe es pot construir en una prova amb un new, sense reflexió ni context; i el constructor que creix fa mal visualment, cosa que converteix l'excés de dependències en un problema visible en lloc de en quinze @Autowired que ningú no compta.

// ❌ Així no: no permet final, no es construeix en un test, amaga el creixement
@Service
public class LloguerService {
    @Autowired private LloguerRepositori lloguerRepositori;
    @Autowired private BicicletaRepositori bicicletaRepositori;
    @Autowired private SelectorTarifa selectorTarifa;
}
// ✅ Així sí
@Service
public class LloguerService {

    private final LloguerRepositori lloguerRepositori;
    private final BicicletaRepositori bicicletaRepositori;
    private final SelectorTarifa selectorTarifa;
    private final Clock rellotge;

    public LloguerService(LloguerRepositori lloguerRepositori,
                          BicicletaRepositori bicicletaRepositori,
                          SelectorTarifa selectorTarifa,
                          Clock rellotge) {
        this.lloguerRepositori = lloguerRepositori;
        this.bicicletaRepositori = bicicletaRepositori;
        this.selectorTarifa = selectorTarifa;
        this.rellotge = rellotge;
    }
}

4.2. Beans sense estat mutable

La pràctica. Un bean singleton no guarda estat que canviï entre peticions.

El perquè. Tots els fils comparteixen la mateixa instància. Un camp mutable en un @Service és una condició de cursa esperant el seu torn, i la fallada apareix sota càrrega, en producció, i és irreproduïble en local.

A CicloUrbana. L'estat per petició viu al MDC o als arguments; l'estat compartit, a la base de dades o a la memòria cau. SelectorTarifa guarda un Map immutable construït al constructor: és estat, però no és mutable.

4.3. No abusis de @Profile

La pràctica. @Profile respon a «on soc?». Quan la pregunta real és «està activada aquesta capacitat?», la resposta és una propietat i @ConditionalOnProperty.

El perquè. Amb @Profile proliferant, activar una funció en preproducció obliga a inventar perfils combinats i el codi acaba sabent on viu, que és exactament el contrari dels dotze factors.

// ❌ El bean sap on viu
@Bean @Profile({"dev", "test", "pre"})
ServeiCorreu serveiCorreuSimulat() { ... }

// ✅ El bean depèn d'una capacitat
@Bean
@ConditionalOnProperty(name = "ciclourbana.correu.mode", havingValue = "simulat",
                       matchIfMissing = true)
ServeiCorreu serveiCorreuSimulat() { ... }

  1. L'API

5.1. DTOs sempre, sense excepcions «només en aquest endpoint»

La pràctica. Cap entitat JPA travessa la frontera HTTP, ni d'entrada ni de sortida.

El perquè. Les quatre fallades de DTOs i Mapatge entre Capes: fuita silenciosa de dades —el contrasenyaHash o el DNI d'un ciutadà de Ribalta—, acoblament invisible que converteix un canvi de nom en un trencament de l'app mòbil, referències circulars en serialitzar relacions i la impossibilitat d'exposar un camp calculat com bicicletesDisponibles. Als quals se suma el tècnic de Transaccions: amb open-in-view: false, una entitat que surt del servei amb relacions mandroses és una LazyInitializationException garantida.

Un DTO és una llista d'inclusions; @JsonIgnore és una llista d'exclusions, i les llistes d'exclusions fallen per omissió.

5.2. Versiona el contracte i valida a la vora

La pràctica. Totes les rutes pengen de /api/v1/..., i tota entrada es valida amb Bean Validation al DTO, amb @Valid al controlador.

El perquè. El prefix de versió no costa res avui i és l'única manera d'introduir un canvi incompatible demà sense trencar l'app instal·lada al mòbil dels ciutadans. I validar a la vora vol dir que el servei mai no rep dades invàlides: la comprovació passa una vegada, en un lloc, declarativament, en lloc de repartida en if per tota la lògica.

5.3. Errors uniformes amb ProblemDetail

La pràctica. Tots els errors surten amb el mateix format RFC 7807, generats en un únic @RestControllerAdvice.

El perquè. Un client que ha d'interpretar cinc maneres diferents d'error acaba fent if (resposta.conte("no existeix")). Un format únic amb un codi propi i un identificador de rastreig converteix el suport en una consulta: el ciutadà llegeix l'identificador a la seva pantalla i l'operador troba la petició exacta als logs.

A CicloUrbana. GestorGlobalExcepcions tradueix la jerarquia de CicloUrbanaException a ProblemDetail, amb el rastreId unificat de Traçabilitat Distribuïda.

5.4. Codis d'estat correctes i paginació obligatòria

La pràctica. 201 amb capçalera Location en crear, 204 en esborrar, 404 si no existeix, 409 en conflicte, 422 si la regla de negoci no es compleix. I cap col·lecció no es retorna sense paginar.

El perquè. Els codis correctes permeten als clients, als proxies i a les mètriques de Monitoratge amb Actuator distingir un error del client d'un del servidor sense llegir el cos. I un findAll() sense Pageable funciona perfectament amb les quatre estacions de Ribalta i tomba l'aplicació el dia que hi hagi quatre-centes mil files de lloguers: és una bomba de rellotgeria que s'arma sola amb el temps.

// ❌ Funciona avui, peta amb el volum
@GetMapping public List<LloguerResponse> llistar() { ... }

// ✅ El límit forma part del contracte
@GetMapping public PaginaResponse<LloguerResponse> llistar(
        @PageableDefault(size = 20) Pageable paginacio) { ... }

  1. Dades i persistència

6.1. La transacció viu al servei

La pràctica. @Transactional(readOnly = true) a la classe de servei, @Transactional explícit als mètodes que escriuen. Mai al controlador.

El perquè. El cas d'ús és la unitat que ha de ser atòmica: iniciar un lloguer marca la bicicleta i crea el registre, o no fa cap de les dues coses. Un mètode de repositori és massa petit per a això; un controlador és massa gran i mantindria la connexió oberta durant la serialització JSON.

I el patró readOnly a la classe té una virtut cultural: escriure es converteix en una decisió conscient. Un mètode que s'oblida d'anotar-se no escriu per accident.

6.2. open-in-view: false, LAZY per defecte, mapatge dins de la transacció

La pràctica. Les tres van juntes i es reforcen.

El perquè. open-in-view: true —el valor per defecte de Spring Boot, i per això cal desactivar-lo explícitament— manté l'EntityManager obert durant la serialització, cosa que amaga els N+1 darrere d'una capa on ningú no els busca i reté una connexió del pool més temps del necessari. Desactivar-ho converteix «el servei retorna DTOs» de bona pràctica en requisit tècnic, que és just el que volem.

spring:
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: validate

6.3. L'esquema és codi: Flyway i ddl-auto: validate

La pràctica. L'esquema es defineix en migracions versionades; Hibernate només valida que la base de dades coincideix amb les entitats.

El perquè. ddl-auto: update no esborra columnes, no reanomena, no omple dades i no deixa constància de quin SQL va executar. És impossible de revisar en una pull request i és impossible de revertir. Amb Flyway, cada canvi d'esquema és un fitxer amb nom, número i checksum, i l'historial viu a flyway_schema_history.

A CicloUrbana. V1__crear_esquema_inicial.sql fins a V9__contract_eliminar_capacitat.sql, més les repetibles R__. És el que es va construir a Migracions d'Esquema amb Flyway.

6.4. Migracions compatibles cap enrere

La pràctica. Una migració ha de funcionar amb la versió de l'aplicació que corre ara i amb la que es desplegarà.

El perquè. Durant un desplegament sense talls conviuen dues versions. Si V9 esborra la columna capacitat i encara hi ha instàncies de la versió anterior que la llegeixen, aquestes instàncies fallen. La solució és el patró expand/contract: primer s'afegeix el que és nou i s'escriuen totes dues columnes (V8__expand_places_totals.sql), després es desplega l'aplicació que només fa servir la nova, i després s'esborra la vella (V9__contract_eliminar_capacitat.sql), en un desplegament diferent.

Operació Compatible? Com fer-la segura
Afegir una columna amb valor per defecte Sí Directa
Afegir una columna NOT NULL sense defecte No Afegir amb defecte, omplir, després endurir
Reanomenar una columna No Expand/contract en dos desplegaments
Esborrar una columna No Només després que cap versió viva la faci servir
Crear un índex en una taula gran Bloqueja escriptures CREATE INDEX CONCURRENTLY

  1. Seguretat

7.1. Denegar per defecte

La pràctica. La cadena de filtres acaba amb anyRequest().denyAll(), no amb permitAll() ni amb res.

El perquè. Amb denegació per defecte, oblidar una regla produeix un 403 visible a la primera prova. Amb permís per defecte, oblidar una regla produeix un endpoint obert que ningú no descobreix fins que algú l'aprofita. El cost de l'error és asimètric, així que el valor per defecte també ho ha de ser.

7.2. Les regles van d'específica a general

La pràctica. L'ordre d'authorizeHttpRequests importa: guanya la primera que coincideix.

// ❌ La segona regla no s'avalua mai: la primera ja ha casat
.requestMatchers("/api/v1/estacions/**").permitAll()
.requestMatchers("/api/v1/estacions/*/manteniment").hasRole("OPERARI")

// ✅ El que és específic, primer
.requestMatchers("/api/v1/estacions/*/manteniment").hasRole("OPERARI")
.requestMatchers(HttpMethod.GET, "/api/v1/estacions/**").permitAll()
.anyRequest().denyAll()

7.3. Seguretat també a nivell de mètode

La pràctica. Les regles per URL són la barrera perimetral; les que depenen de la dada viuen al servei, amb @PreAuthorize.

El perquè. «Només els teus propis lloguers» no és una propietat de la ruta. I una regla al servei s'aplica a tots els punts d'entrada, inclosos els que encara no existeixen: la tasca programada, el consumidor de missatges o l'endpoint que algú afegeixi d'aquí a sis mesos.

@PreAuthorize("hasAnyRole('OPERARI','ADMIN') or "
            + "@seguretatLloguers.esPropietari(#idLloguer, principal)")
@Transactional
public LloguerResponse finalitzar(Long idLloguer, FinalitzarLloguerRequest peticio) { ... }

7.4. Mínim privilegi i mai registrar credencials

La pràctica. Cada rol té el just, i cap log conté una contrasenya, un token o una capçalera Authorization, ni tan sols en DEBUG.

El perquè. Els logs es copien a sistemes d'agregació, s'envien a tercers i es conserven anys. Un token en un log és una credencial circulant durant mesos abans que algú se n'adoni.

  1. Proves

8.1. La piràmide, i la unitat no aixeca el context

La pràctica. Moltes proves unitàries ràpides, algunes de llesca, poques d'integració, poquíssimes d'extrem a extrem.

El perquè. Una suite de cinc segons s'executa a cada desada; una de vint-i-cinc minuts deixa d'executar-se, i una suite que no s'executa no protegeix de res. @SpringBootTest per provar una fórmula de tarifa multiplica per mil el temps i no detecta ni una fallada més.

// ❌ Context sencer per comprovar una multiplicació
@SpringBootTest
class TarifaEstandardTest {
    @Autowired TarifaEstandard tarifa;
    @Test void calcula() { ... }        // 4 segons
}

// ✅ Sense Spring
class TarifaEstandardTest {
    @Test void cobraDesbloqueigMesDotzeCentimsPerMinut() {
        assertThat(new TarifaEstandard().calcular(Duration.ofMinutes(30)))
                .isEqualByComparingTo("4.10");        // 0,8 ms
    }
}

8.2. Ràpides i deterministes

La pràctica. Cap prova no depèn del rellotge del sistema, de l'ordre d'execució ni de dades que va deixar una altra prova.

El perquè. Una prova intermitent és pitjor que cap: entrena l'equip a reintentar en lloc d'investigar, i aquest costum acaba ignorant també les fallades reals.

A CicloUrbana. El Clock injectat a LloguerService, ServeiJwt i CaducadorLloguers no és elegància: és el que permet Clock.fixed(Instant.parse("2026-09-01T23:00:00Z"), ZoneOffset.UTC) i afirmar sobre un instant exacte.

8.3. Testcontainers per al que depèn de la base de dades real

La pràctica. Les proves que verifiquen migracions, índexs, restriccions o SQL natiu aixequen un PostgreSQL real i efímer, no H2.

El perquè. H2 en mode compatibilitat no és PostgreSQL: difereix en tipus, en funcions, en el comportament dels índexs parcials i en el dels bloqueigs. Una prova que passa a H2 i falla en producció és exactament la fallada que les proves havien d'evitar.

I la contrapartida honesta: són lentes. Per això viuen en classes *IT executades per Failsafe a ./mvnw verify, separades del cicle curt de ./mvnw test.

  1. Operació

9.1. Sondes de salut diferenciades

La pràctica. liveness respon «el procés està sa»; readiness respon «puc rebre trànsit». No són el mateix.

El perquè. Confondre-les produeix dues fallades oposades i totes dues greus. Si liveness comprova la base de dades, un tall de PostgreSQL fa que Kubernetes reiniciï totes les rèpliques d'una aplicació que estava perfectament sana, convertint una degradació en una caiguda. Si readiness no comprova res, el balancejador envia trànsit a una instància que encara no ha acabat d'arrencar.

9.2. Logs estructurats a stdout

La pràctica. JSON per la sortida estàndard, amb el rastreId a cada línia. Ni fitxers ni rotació gestionada per l'aplicació.

El perquè. En un contenidor, escriure a un fitxer és escriure en un disc efímer que desapareix amb el pod. I un log en JSON és consultable: | json | rastreId = "..." retorna la història completa d'una petició; un log en text lliure obliga a expressions regulars fràgils.

9.3. Mètriques de negoci, no només tècniques

La pràctica. A més de latència i memòria, es mesuren els fets del negoci: lloguers iniciats per tarifa, finalitzats, ocupació d'estacions.

El perquè. Les mètriques tècniques diuen que l'aplicació funciona; les de negoci diuen que serveix. Un desplegament que no trenca res però deixa els lloguers iniciats a zero és una fallada que cap mètrica de JVM no detecta.

Amb la regla que les fa viables: cardinalitat baixa a les etiquetes. MetriquesCicloUrbana etiqueta per tarifa i per estacio —tres i quatre valors—, mai per idUsuari.

9.4. Aturada ordenada i desplegament reversible

La pràctica. server.shutdown: graceful, executors que esperen a acabar, i una estratègia de desplegament que permeti tornar enrere en minuts.

El perquè. Un SIGKILL enmig d'una petició deixa el ciutadà amb un error i, si hi havia una transacció a mitges, amb un estat que cal reconciliar. I sobre el segon punt: la mètrica DORA que més es descuida és el temps de restauració, i la manera més barata de millorar-lo és que revertir sigui un botó i no una investigació.

  1. La llista de comprovació d'una aplicació llesta per a producció

Aquesta és la taula que pots endur-te a qualsevol projecte. Es recorre sencera abans del primer desplegament a producció, i després un cop per trimestre.

# Àrea Comprovació Lliçó
1 Estructura Paquets per funcionalitat, no per capa 01-04
2 Estructura La classe principal és al paquet arrel 01-04
3 Estructura Cap controlador no accedeix a un repositori 10-03
4 Estructura El domini no importa classes del framework web 02-02
5 Configuració Zero secrets al repositori i al seu historial 07-02
6 Configuració Un sol artefacte per a tots els entorns 07-02
7 Configuració Propietats agrupades en @ConfigurationProperties validades 02-05
8 Configuració El que és obligatori no té valor per defecte 02-05
9 Configuració El perfil s'activa per entorn i està verificat al log d'arrencada 07-02
10 Beans Injecció per constructor i camps final a tot el projecte 02-02
11 Beans Cap singleton amb estat mutable 02-03
12 API DTOs d'entrada i sortida a tots els endpoints 03-05
13 API Rutes versionades (/api/v1/...) 03-01
14 API @Valid a tots els cossos de petició 03-04
15 API Errors uniformes amb ProblemDetail i sense traça de pila 03-06
16 API Codis d'estat correctes, amb Location a les creacions 03-03
17 API Cap col·lecció no es retorna sense paginar 04-05
18 API La documentació OpenAPI està tancada en producció 03-07
19 Dades @Transactional al servei, readOnly a les lectures 04-07
20 Dades open-in-view: false 04-02
21 Dades Totes les associacions són LAZY 04-04
22 Dades Flyway governa l'esquema i ddl-auto: validate 04-08
23 Dades Les migracions pendents són compatibles cap enrere 04-08
24 Dades El pool està dimensionat amb criteri, no «per si de cas» 09-01
25 Seguretat anyRequest().denyAll() tanca cada cadena 05-02
26 Seguretat Regles ordenades d'específica a general, revisades una a una 05-02
27 Seguretat Regles per dada amb @PreAuthorize a cada recurs d'usuari 05-05
28 Seguretat Contrasenyes amb BCrypt i JWT curt, rotable i sense dades sensibles 05-04
29 Seguretat Cap log no conté tokens, contrasenyes ni dades personals 09-05
30 Proves La suite ràpida acaba en segons i s'executa a cada canvi 06-01
31 Proves Cada regla de seguretat té la seva prova automàtica 06-04
32 Proves El que depèn de PostgreSQL es prova amb Testcontainers 06-05
33 Operació Sondes liveness i readiness diferenciades i enganxades a l'orquestrador 07-01
34 Operació Logs en JSON a stdout, amb rastreId a cada línia 09-05
35 Operació Mètriques de negoci publicades, amb etiquetes de baixa cardinalitat 09-03
36 Operació Aturada ordenada configurada i coherent amb el termini de l'orquestrador 07-03
37 Operació Alertes sobre símptomes —latència, errors— i no sobre causes 09-04
38 Lliurament Revertir a la versió anterior és una ordre, no una investigació 08-05

  1. Quan trencar la regla

Aquest apartat és el que dona valor als deu anteriors. Cadascuna d'aquestes pràctiques té un context on deixa de ser la millor opció, i saber quin és la diferència entre aplicar-les i entendre-les.

Pràctica Es pot trencar quan... El que cal fer a canvi
DTOs sempre Un microservei intern amb un únic client que ets tu i el «domini» del qual és literalment el contracte Documentar-ho; i tan bon punt hi hagi un segon client, introduir el DTO
Interfície per a cada servei Gairebé sempre: una interfície amb una sola implementació i sense frontera de mòdul és cerimònia Res. És la regla que més s'aplica sense pensar
Paginació obligatòria La col·lecció té una mida acotada per disseny (els quatre estats d'una bicicleta) Assegurar-se que la cota és estructural, no «avui són pocs»
Transacció només al servei Un importador que necessita una transacció per fila TransactionTemplate, amb el motiu comentat
Mai @PostFilter Col·lecció petita, acotada i no paginada Verificar que ho continua sent d'aquí a un any
Tot LAZY Una relació @ManyToOne que sempre es necessita i sempre és una fila Mesurar; gairebé sempre és millor un @EntityGraph puntual
Piràmide de proves Una capa que és pur cablejat i on la prova d'integració és l'única útil No convertir-ho en la norma: és l'origen del con de gelat
Monòlit modular Una part amb un perfil d'escalat radicalment diferent o un requisit d'aïllament Tenir traçabilitat distribuïda abans de partir
Un artefacte per a tots els entorns Mai. Aquesta no es trenca —
Secrets fora del repositori Mai. Aquesta tampoc —

Les dues últimes files són deliberades: hi ha regles que no tenen excepció defensable, i convé tenir clar quines són. Tota la resta és un compromís, i un compromís es pren amb els costos a la vista.

La manera honesta de trencar una regla té tres passos: anomenar la regla que s'està trencant, dir què s'hi guanya, i dir què s'hi perd i com es compensa. Un comentari de tres línies sobre el codi, o una entrada al registre de decisions del projecte. El que no val és trencar-la sense adonar-se'n.

Errors Comuns i Consells

Aplicar una pràctica sense entendre quin problema resol. És l'error de fons d'aquesta lliçó. Es manifesta en interfícies amb una implementació, en @Profile per a tot, en proves que només existeixen per pujar la cobertura i en una capa de mapejadors que mapeja record idèntics.

Convertir la llista de comprovació en un tràmit. Marcar 38 caselles sense verificar-ne cap és pitjor que no tenir llista, perquè produeix la sensació d'haver-ho revisat. Cada punt necessita una comprovació objectiva: no «em sembla que Swagger està tancat», sinó curl contra /swagger-ui.html en producció.

Confondre «funciona» amb «està bé». ddl-auto: update funciona, exposar entitats funciona, @Autowired en camps funciona. Totes les pràctiques d'aquesta lliçó es refereixen al que passa després: al sisè mes, amb el volum real, amb tres persones més tocant el codi.

Introduir totes les pràctiques de cop en un projecte existent. Un canvi massiu és irrevisable i arriscat. L'ordre útil en un projecte heretat: primer el que evita dany irreversible (secrets, denegació per defecte, migracions), després el que dona xarxa de seguretat (proves), i per últim el que és estructural (paquets, DTOs), fitxer a fitxer i aprofitant els canvis que ja calgui fer.

Consell: escriu el perquè al repositori, no al cap. Un fitxer docs/decisions/ amb una entrada curta per decisió —context, opcions, elecció, conseqüències— val més que qualsevol documentació generada. D'aquí a un any, la pregunta no serà «què fa això» sinó «per què es va fer així», i la resposta se sol haver perdut.

Consell: converteix en automàtic tot el que es pugui. Una pràctica que depèn que algú se'n recordi en una revisió s'incompleix tard o d'hora. unmappedTargetPolicy=ERROR a MapStruct, ddl-auto: validate, denyAll() al final, failBuildOnCVSS a Dependency-Check i les regles d'ArchUnit de 10-03 són la mateixa idea: moure la comprovació del cap d'una persona a la construcció.

Consell: una pràctica que no pots explicar en dues frases no la domines. Prova-ho amb open-in-view: false, amb readOnly = true o amb l'ordre de les regles de seguretat. Si l'explicació no surt, torna a la lliçó corresponent: és més rendible que memoritzar la regla.

Exercicis

Exercici 1: auditar un servei amb la llista de comprovació

Un equip et passa aquesta classe d'un altre projecte Spring Boot per revisar-la. Identifica totes les pràctiques d'aquesta lliçó que incompleix, agrupades per àrea, i indica per a cadascuna quin problema concret causarà i en quin moment.

package com.exemple.service;

@Service
public class ComandaService {

    @Autowired private ComandaRepository comandaRepository;
    @Autowired private ClientRepository clientRepository;
    @Value("${passarela.clau:clau-de-proves}") private String clauPassarela;

    private int comandesProcessades = 0;

    @GetMapping("/comandes")
    public List<Comanda> llistar() {
        return comandaRepository.findAll();
    }

    public Comanda crear(Comanda comanda) {
        Client client = clientRepository.findById(comanda.getClientId()).get();
        comanda.setClient(client);
        comandesProcessades++;
        log.info("Creant comanda per a {} amb clau {}", client.getCorreu(), clauPassarela);
        return comandaRepository.save(comanda);
    }
}

Exercici 2: justificar una excepció

L'ajuntament de Ribalta demana un endpoint intern GET /api/v1/intern/estacions/complet que retorna, per al panell d'operaris, tota la informació d'una estació: els seus camps, les seves bicicletes amb el codi d'ancoratge intern, les últimes incidències i els comptadors d'auditoria. Un company proposa retornar directament l'entitat Estacio perquè «és intern i estalvia tres classes».

Argumenta si l'excepció és defensable. Si creus que no, proposa l'alternativa concreta. Si creus que sí en algun cas, digues sota quines condicions i què caldria fer a canvi.

Exercici 3: la llista de comprovació aplicada a Ribalta

Redacta l'informe de les vuit comprovacions de la taula de l'apartat 10 que consideraries més crítiques per al primer desplegament de CicloUrbana a producció, ordenades per criticitat. Per a cadascuna indica: com es verifica de manera objectiva (una ordre, una consulta, una prova) i què faries si la comprovació falla la nit abans del desplegament.

Solucions

Solució 1

Àrea d'estructura i capes.

  1. Un @GetMapping dins d'un @Service. La classe barreja dues capes: és controlador i servei alhora. Conseqüència: no hi ha on posar la transacció sense que abasti la serialització, i no es pot provar la lògica sense MockMvc. Quan fa mal: tan bon punt hi hagi un segon punt d'entrada.
  2. El paquet és com.exemple.service, organització per capa. Conseqüència: cada canvi funcional toca quatre carpetes i la visibilitat de paquet deixa de servir com a frontera.

Àrea de beans i injecció.

  1. @Autowired en camps. No permet final, obliga a reflexió per construir la classe en una prova i amaga el creixement de dependències.
  2. private int comandesProcessades: estat mutable en un singleton. Conseqüència: condició de cursa, el comptador perd increments sota concurrència i la fallada és irreproduïble en local. Si de debò es vol aquest número, és un Counter de Micrometer.

Àrea de configuració i seguretat.

  1. @Value solt per a un secret. Hauria de ser un @ConfigurationProperties validat.
  2. El secret té valor per defecte (clau-de-proves). Conseqüència: l'aplicació arrenca en producció sense la clau real i falla al primer cobrament, en lloc de fallar a l'arrencada.
  3. Es registra el secret al log, i a més el correu del client. Conseqüència: una credencial i una dada personal viatjant a l'agregació de logs i conservades durant anys. És la fallada més greu de la classe.

Àrea d'API i dades.

  1. Retorna l'entitat Comanda i la rep com a paràmetre. Fuita de dades a la sortida i mass assignment a l'entrada: el client pot fixar qualsevol camp, inclòs l'id o l'estat.
  2. findAll() sense paginar. Funciona amb 50 comandes i tomba la JVM amb 500.000.
  3. Cap anotació transaccional. crear fa dues operacions d'escriptura que es confirmen per separat; si la segona falla, queda un estat inconsistent sense cap error visible.
  4. .get() sobre un Optional. Si el client no existeix, NoSuchElementException i un 500 amb traça en lloc d'un 404 amb ProblemDetail.
  5. comanda.setClient(client) amb l'entitat rebuda del client HTTP, que és una entitat no gestionada barrejada amb una de gestionada: comportament impredictible en persistir.

El patró de fons: cap d'aquests dotze problemes no produeix un error avui. Tots produeixen un error d'aquí a uns mesos, i uns quants de manera silenciosa. Aquesta és la definició operativa de deute tècnic.

Solució 2

L'excepció no és defensable tal com està plantejada, i el motiu principal no és el que se sol donar.

L'argument habitual contra exposar entitats és la fuita de dades, i aquí el company el neutralitza dient que l'endpoint és intern. Però hi ha tres raons que continuen dretes:

1. «Intern» no vol dir «sense clients». El panell d'operaris és un client, el manté un altre equip i es desplega pel seu compte. Tan bon punt algú reanomeni capacitat a placesTotals a l'entitat —exactament el que vam fer a V8__expand_places_totals.sql— el panell deixa de mostrar la capacitat, sense cap error de compilació en cap dels dos costats.

2. El problema tècnic és insalvable. Amb open-in-view: false, retornar Estacio amb les seves bicicletes, les seves incidències i la seva auditoria produeix LazyInitializationException a la serialització, tret que es carreguin totes les relacions dins de la transacció. I si es carreguen totes, hem construït un agregat sense límit: la resposta creix amb l'històric d'incidències sense cap cota.

3. L'entitat porta camps que no són dades. @Version, els camps d'EntitatAuditable i les relacions bidireccionals existeixen per raons de persistència, no de contracte. Publicar-los convida el client a interpretar-los.

L'alternativa concreta, que a més costa menys del que sembla:

// Vista interna, al paquet d'operacions, amb la seva pròpia ruta i el seu propi rol
public record EstacioOperariResponse(
        Long id, String nom, String adreca, int capacitat,
        UbicacioResponse ubicacio,
        int ancoratgesLliures, boolean esPlena,
        List<BicicletaOperariResum> bicicletes,      // amb codiAncoratgeIntern
        List<IncidenciaResum> incidenciesObertes,    // acotat: només les obertes
        Instant ultimaRevisio) {}

Tres decisions dins de l'alternativa: les incidències que s'imbriquen són només les obertes, que estan acotades per disseny, mentre que l'històric complet es consulta paginat a /api/v1/intern/estacions/{id}/incidencies; és una classe diferent de la pública, no la mateixa amb camps condicionals, perquè un if (esOperari) dins d'un mapejador falla el dia que algú inverteixi la condició; i la ruta i el rol són diferents, de manera que la protecció no depèn que ningú no s'equivoqui en mapejar.

Hi ha algun cas on sí que seria defensable? Un, i molt acotat: un endpoint de diagnòstic sota /actuator, protegit per rol ADMIN, el propòsit explícit del qual sigui bolcar l'estat intern per depurar, documentat com a no estable i exclòs de la documentació pública. Això no és una API: és una eina. I encara així, amb la comprovació prèvia que no publica cap dada personal.

Solució 3

Ordre Comprovació Verificació objectiva Si falla la nit abans
1 Zero secrets al repositori i al seu historial (#5) gitleaks detect --log-opts="--all" sobre l'historial complet, no només l'última versió Es para el desplegament. Rotar tots els secrets trobats abans de res; esborrar-los del codi no n'hi ha prou
2 El perfil prod s'activa de debò (#9) Arrencar amb la configuració real i buscar al log The following 1 profile is active: "prod". Si diu falling back to default, tota la resta d'aquesta taula és falsa Es para: sense perfil actiu, l'aplicació corre amb la configuració de desenvolupament, inclosos Swagger obert i H2
3 anyRequest().denyAll() i regles revisades (#25, #26) Prova automatitzada que llança peticions sense token a una llista de rutes conegudes i a rutes inventades: totes han de donar 401 o 403, cap 200 Es para. És l'única fallada d'aquesta llista que exposa dades de tercers de manera immediata
4 Regles per dada amb @PreAuthorize (#27) Prova d'integració amb dos ciutadans reals creuant identificadors en lloguers i incidències: tot 403 Es para si afecta dades personals; l'IDOR és la fallada més explotada de les APIs
5 Flyway governa l'esquema i ddl-auto: validate (#22, #23) flyway:info contra una còpia de l'esquema de producció, i revisar que les migracions pendents són compatibles cap enrere Es posposa el desplegament: una migració incompatible durant un desplegament progressiu trenca les instàncies de la versió anterior
6 Sondes diferenciades i enganxades (#33) curl a /actuator/health/liveness i /readiness; i comprovar al manifest que livenessProbe no consulta la base de dades Es pot desplegar amb vigilància manual, però es corregeix immediatament: una sonda mal configurada converteix una degradació en una caiguda total
7 Revertir és una ordre (#38) Executar de debò el rollback en preproducció i cronometrar-lo Es pot desplegar, però només en horari de baixa activitat i amb algú disponible
8 Cap log amb credencials ni dades personals (#29) Executar un flux complet en preproducció i buscar als logs eyJ, Bearer, contrasenya i un correu conegut: zero resultats Es corregeix abans; no bloqueja si el sistema de logs encara no exporta a tercers, però s'arregla al desplegament següent

El criteri d'ordenació, que és el fons de l'exercici: primer el que compromet tot el sistema de cop (un secret filtrat, la configuració de desenvolupament en producció), després el que compromet dades de tercers (denegació per defecte, accés creuat), després el que compromet la integritat de les dades (migracions), i per últim el que compromet la capacitat de reaccionar (sondes, reversió, logs). És la mateixa escala que vam fer servir a 05-05, aplicada ara a tot el sistema i no només a la seguretat.

I una observació que convé interioritzar: dels vuit punts, sis es verifiquen executant alguna cosa, no llegint codi. Una llista de comprovació els punts de la qual es marquen per inspecció visual és una llista de bones intencions.

Conclusió

Les decisions que hem anat prenent durant nou mòduls tenen ara una forma reconeixible. Saps que una bona pràctica és la resposta que sol ser correcta a un problema recurrent en un context concret, i que les tres paraules importen: sense el problema, la pràctica és superstició; sense el context, és un dogma. I saps que gairebé totes es deriven de tres principis de fons —fer explícit l'implícit, fallar aviat i sorollosament, separar el que canvia per raons diferents— que serveixen de brúixola davant de decisions que cap catàleg no cobreix.

Tens el catàleg complet agrupat per àrea. Estructura: paquets per funcionalitat, monòlit modular amb fronteres reals, la regla de dependència controlador → servei → repositori que mai no s'inverteix, i un domini que no importa el framework perquè això és el que el fa durador i comprovable en mil·lisegons. Configuració: @ConfigurationProperties validades enfront de @Value escampat, un sol artefacte per a tots els entorns, secrets sempre fora del repositori i sense valor per defecte per al que és obligatori. Beans: constructor, final, sense @Autowired, sense estat mutable i sense @Profile per al que en realitat és una capacitat. API: DTOs sense excepcions, versionat, validació a la vora, ProblemDetail uniforme, codis correctes i paginació obligatòria. Dades: la transacció al servei amb readOnly per defecte, open-in-view: false amb LAZY i mapatge dins de la transacció com un sol paquet de decisions, Flyway amb validate i migracions compatibles cap enrere. Seguretat: denegar per defecte, ordre d'específica a general, regles per dada al servei i mai una credencial en un log. Proves: la piràmide, la unitat sense context, el determinisme del Clock injectat i Testcontainers per al que H2 no pot validar. Operació: sondes diferenciades, logs estructurats a stdout, mètriques de negoci amb cardinalitat controlada, aturada ordenada i desplegament reversible.

I tens les dues peces que converteixen el catàleg en una eina de treball: la llista de trenta-vuit comprovacions amb la seva lliçó de referència, pensada per recórrer-se abans d'un desplegament i un cop per trimestre; i l'apartat de quan trencar la regla, amb la taula d'excepcions defensables, les dues que no tenen excepció —un artefacte per a tots els entorns i els secrets fora del repositori— i la manera honesta de trencar qualsevol altra: anomenar la regla, dir què s'hi guanya i dir què s'hi perd i com es compensa.

Tot aquest catàleg està escrit en positiu: el que convé fer. Però la majoria de nosaltres no aprenem així. Aprenem quan alguna cosa falla, i les pràctiques d'aquesta lliçó són en realitat la cicatriu d'errors concrets que algú va cometre abans: l'anotació que no va fer res, l'endpoint que va quedar obert per l'ordre de dues línies, l'excepció capturada que es va endur per davant el rollback, la tasca programada que es va executar tres vegades en escalar. La lliçó següent, Errors Comuns i Com Evitar-los, recorre aquest revers: un catàleg de fallades reals amb el seu símptoma, la seva causa, com es diagnostica i el codi corregit —des de la classe principal al paquet equivocat fins a la memòria cau que amaga una consulta mal escrita—, amb el parany del proxy tractat per fi d'una vegada i en un sol lloc, i una taula de diagnòstic ràpid que va del símptoma a la lliçó on és la resposta.

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