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
- Una pràctica no és un dogma
- Estructura i disseny
- Configuració
- Injecció i beans
- L'API
- Dades i persistència
- Seguretat
- Proves
- Operació
- La llista de comprovació d'una aplicació llesta per a producció
- Quan trencar la regla
- Errors Comuns i Consells
- Exercicis
- 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.
- 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
└── comuA 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.
- 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} |
- 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() { ... }
- 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) { ... }
- 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.
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 |
- 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.
- 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.
- 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ó.
- 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 |
- 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.
- Un
@GetMappingdins 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 senseMockMvc. Quan fa mal: tan bon punt hi hagi un segon punt d'entrada. - 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ó.
@Autowireden camps. No permetfinal, obliga a reflexió per construir la classe en una prova i amaga el creixement de dependències.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 unCounterde Micrometer.
Àrea de configuració i seguretat.
@Valuesolt per a un secret. Hauria de ser un@ConfigurationPropertiesvalidat.- 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. - 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.
- Retorna l'entitat
Comandai 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'ido l'estat. findAll()sense paginar. Funciona amb 50 comandes i tomba la JVM amb 500.000.- Cap anotació transaccional.
crearfa dues operacions d'escriptura que es confirmen per separat; si la segona falla, queda un estat inconsistent sense cap error visible. .get()sobre unOptional. Si el client no existeix,NoSuchElementExceptioni un500amb traça en lloc d'un404ambProblemDetail.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
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
