«Has après a fer servir les eines. Ara construiràs alguna cosa amb elles.»

Amb aquesta frase acabava el mòdul 11, i aquesta lliçó és la primera que se la pren de debò. Perquè hi ha una diferència enorme entre saber fer servir Spring, JPA, JUnit, Maven, Jackson i Logback i tenir un projecte. BiblioTech, ara mateix, és el primer: un conjunt de peces excel·lents que viuen totes juntes en un únic mòdul Maven, en paquets que s'han anat creant per acumulació, on una entitat JPA pot importar HttpClient, un servei de domini pot importar org.springframework, i res —absolutament res— no impedeix que demà algú fiqui una consulta SQL dins de CalculadoraMultes.

Això funciona. Amb onze mòduls de curs a sobre, funciona. El problema no és que no funcioni avui: és que no resisteix el creixement. Un projecte sense fronteres es degrada de manera previsible, i el mecanisme sempre és el mateix: algú té pressa, la classe que necessita és a un import de distància, i no hi ha res que l'hi impedeixi. Repeteix això dues-centes vegades i tindràs el que a la indústria s'anomena, sense gens d'afecte, «la bola de fang».

Aquesta lliçó converteix BiblioTech en un projecte professional. No hi afegeix ni una funcionalitat. Hi afegeix estructura: una arquitectura explícita, unes fronteres que el compilador verifica, una configuració per entorn, un control de versions ordenat, un estil automàtic i una documentació que serveix de debò.

En acabar sabràs dissenyar l'estructura d'un projecte Java real, entendràs l'arquitectura per capes i l'hexagonal i sabràs quan cal fer servir cadascuna, organitzaràs paquets amb criteri, convertiràs un projecte en multimòdul Maven de manera que el mateix graf de dependències impedeixi físicament els errors arquitectònics, separaràs DTO d'entitats, configuraràs l'aplicació per entorn sense filtrar secrets, i deixaràs el repositori en un estat en què una altra persona pugui clonar-lo i arrencar-lo en cinc minuts.

Contingut

  1. El problema: què li passa a un projecte sense estructura
  2. Què és l'arquitectura d'una aplicació
  3. Arquitectura per capes
  4. La regla de dependència
  5. Arquitectura hexagonal: ports i adaptadors
  6. Comparativa: capes enfront d'hexagonal
  7. Organització de paquets: per capa enfront de per funcionalitat
  8. L'arbre de paquets de BiblioTech, en les dues opcions
  9. Recomanació justificada
  10. Projecte multimòdul Maven aplicat a BiblioTech
  11. El POM pare i dependencyManagement
  12. Com el graf de mòduls impedeix que el domini importi Spring
  13. DTO enfront d'entitats
  14. Configuració per entorn: application.yml i perfils
  15. La jerarquia de fonts de configuració de Spring Boot
  16. Variables d'entorn i secrets fora del repositori
  17. Control de versions: .gitignore, branques i commits convencionals
  18. Format i estil: .editorconfig i Spotless
  19. Un README.md que serveixi de debò
  20. Registre de decisions d'arquitectura (ADR)
  21. Scripts d'arrencada i dependències locals
  22. L'arbre complet de BiblioTech reestructurat
  23. Errors Comuns i Consells
  24. Exercicis
  25. Conclusió

  1. El problema: què li passa a un projecte sense estructura

Abans de proposar solucions, convé veure el problema amb precisió. Aquest és un fragment real del BiblioTech actual:

package com.nexussoftware.bibliotech.servei;

import com.nexussoftware.bibliotech.model.Prestec;
import com.nexussoftware.bibliotech.repositori.PrestecRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.net.http.HttpClient;   // que hi fa, aixo, aqui?

@Service
public class GestorPrestecs {

    private final PrestecRepository repositori;
    private final HttpClient http;   // un servei de negoci amb un client HTTP a dins
    // ...
}

Cada línia d'aquesta classe és defensable per separat. El conjunt no ho és:

Símptoma Conseqüència a mitjà termini
La lògica de negoci importa org.springframework No la pots provar sense aixecar un context; migrar de framework és reescriure
La lògica de negoci importa java.net.http Per provar el càlcul de multes necessites xarxa o un mock d'HTTP
Tot és en un mòdul Maven Res no impedeix que l'entitat Prestec cridi el repositori, ni que el repositori cridi el controlador
Les entitats JPA viatgen a l'exterior Canviar una columna trenca els clients de l'API
No hi ha direcció de dependències declarada Apareixen cicles: serveiwebservei

El símptoma final sempre és el mateix: el temps que costa fer un canvi petit creix. I creix perquè qualsevol canvi pot trencar qualsevol cosa, perquè no hi ha manera de raonar sobre una part sense conèixer el tot.

L'arquitectura és, exactament, el conjunt de decisions que limiten el que es pot fer. Una bona arquitectura no et dona poders: et treu opcions dolentes.

  1. Què és l'arquitectura d'una aplicació

Definició operativa, sense misticisme:

L'arquitectura d'una aplicació és la divisió del sistema en parts, l'assignació de responsabilitats a cada part i les regles sobre quina part pot dependre de quina.

Les tres coses importen, però la tercera és la que s'oblida i l'única que es degrada tota sola. Dividir en model, servei i web és fàcil; el difícil és que d'aquí a sis mesos model continuï sense dependre de web.

Hi ha dues preguntes que tota arquitectura respon:

  1. On viu la lògica de negoci? (les regles que existirien encara que no hi hagués ordinadors: un préstec dura 15 dies, una multa són 0,50 € per dia, un empleat no pot tenir més de 3 préstecs actius)
  2. Com s'aïlla aquesta lògica de la tecnologia? (Spring, JPA, HTTP, PostgreSQL, JSON… tot això és detall: canvia cada pocs anys)

Les dues arquitectures que veurem responen igual a la primera i de manera diferent a la segona.

  1. Arquitectura per capes

És l'organització clàssica, i probablement la que sosté més aplicacions Java al món. El sistema es divideix en capes horitzontals, i cada capa només pot cridar la immediatament inferior.

flowchart TD
    P["Presentacio<br/>REST, CLI, vistes"]
    A["Aplicacio / Servei<br/>casos d'us, transaccions"]
    D["Domini<br/>entitats, regles de negoci"]
    I["Infraestructura<br/>JPA, HTTP, fitxers, SMTP"]

    P --> A
    A --> D
    A --> I
    I --> D

    style D fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

Responsabilitat de cada capa a BiblioTech:

Capa Què conté a BiblioTech Què NO pot contenir
Presentació PrestecController (REST), ordres Picocli, formatadors de sortida Regles de negoci, consultes a base de dades
Aplicació / Servei GestorPrestecs, ProcessadorReserves, ServeiAvisos, transaccions SQL, JSON, HTTP, detalls de presentació
Domini Material, Llibre, Prestec, Empleat, CalculadoraMultes, Gravetat Absolutament res extern
Infraestructura Repositoris JPA, ClientMetadades (HTTP), enviament de correu, lectura de fitxers Regles de negoci

Un exemple concret de repartiment correcte, amb el cas d'ús «retornar un préstec»:

// PRESENTACIO: converteix la peticio HTTP en una crida al servei. Res mes.
@PostMapping("/api/prestecs/{id}/devolucio")
public ResponseEntity<FitxaPrestec> retornar(@PathVariable Long id) {
    return ResponseEntity.ok(gestorPrestecs.retornar(id));
}

// APLICACIO: orquestra, delimita la transaccio, no decideix regles.
@Transactional
public FitxaPrestec retornar(Long id) {
    Prestec prestec = repositori.findById(id)
        .orElseThrow(() -> new PrestecNoTrobatException(id));
    Diner multa = prestec.registrarDevolucio(LocalDate.now(rellotge));  // la regla viu al domini
    avisos.notificarDevolucio(prestec);
    return FitxaPrestec.desDe(prestec);
}

// DOMINI: la regla de negoci. Sense Spring, sense JPA visible, sense HTTP.
public Diner registrarDevolucio(LocalDate data) {
    if (this.dataDevolucio.isPresent()) {
        throw new PrestecJaRetornatException(this.id);
    }
    this.dataDevolucio = Optional.of(data);
    this.estat = EstatPrestec.RETORNAT;
    return CalculadoraMultes.calcular(this.dataVenciment, data);
}

Fixa't on és cada decisió. El controlador no sap què és una multa. El servei no sap com es calcula. El domini no sap que existeix HTTP. Cada capa sap el just.

  1. La regla de dependència

És el cor de tot el que ve, així que va destacat:

El domini no depèn de res. Tota la resta depèn del domini.

La conseqüència immediata sembla un problema: si el servei d'aplicació necessita desar un Prestec a la base de dades, i la base de dades és infraestructura, no està el domini depenent de la infraestructura?

No, si s'inverteix la dependència. És la D de SOLID (inversió de dependències, que veurem formalment a 12-02) i ja la vas fer servir a 11-02 sense aquest nom:

// AL DOMINI: una interficie que el domini defineix perque el domini la necessita.
package com.nexussoftware.bibliotech.domini.port;

public interface RepositoriPrestecs {
    Optional<Prestec> cercarPerId(Long id);
    Prestec guardar(Prestec prestec);
    List<Prestec> vencutsA(LocalDate data);
}
// A LA INFRAESTRUCTURA: la implementacio, que si que coneix JPA.
package com.nexussoftware.bibliotech.infraestructura.persistencia;

@Repository
class RepositoriPrestecsJpa implements RepositoriPrestecs {

    private final PrestecSpringDataRepository delegat;   // Spring Data, modul 11

    RepositoriPrestecsJpa(PrestecSpringDataRepository delegat) {
        this.delegat = delegat;
    }

    @Override
    public Optional<Prestec> cercarPerId(Long id) {
        return delegat.findById(id);
    }
    // ...
}

La fletxa de compilació va d'infraestructura a domini (RepositoriPrestecsJpa importa RepositoriPrestecs), encara que la crida en execució vagi del domini a la infraestructura. La direcció de la dependència i la direcció del flux són coses diferents, i aquesta és la idea més important de la lliçó.

flowchart LR
    S["GestorPrestecs<br/>(aplicacio)"]
    Pu["RepositoriPrestecs<br/>(interficie, domini)"]
    Im["RepositoriPrestecsJpa<br/>(infraestructura)"]

    S -->|"fa servir"| Pu
    Im -.->|"implementa"| Pu

    style Pu fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

Ningú del domini ni de l'aplicació no escriu mai import ...infraestructura....

  1. Arquitectura hexagonal: ports i adaptadors

L'arquitectura hexagonal (Alistair Cockburn, 2005; també anomenada ports i adaptadors) porta la idea anterior fins al final. La seva tesi:

L'aplicació té un interior (domini i casos d'ús) i un exterior (tot el que la toca). L'interior no sap res de l'exterior. La comunicació passa sempre per ports (interfícies definides per l'interior), i cada tecnologia concreta és un adaptador que s'endolla a un port.

Hi ha dos tipus de ports:

  • Ports d'entrada (driving): el que l'aplicació ofereix. Els adaptadors que els fan servir són els qui condueixen l'aplicació: REST, CLI, una prova, un consumidor de missatges.
  • Ports de sortida (driven): el que l'aplicació necessita. Els adaptadors que els implementen són conduïts per l'aplicació: JPA, HTTP, SMTP, sistema de fitxers.
flowchart LR
    subgraph EXT_ESQ["Adaptadors d'entrada"]
        REST["REST<br/>PrestecController"]
        CLI["CLI<br/>OrdrePrestec"]
        SOCK["Socket<br/>ServidorCataleg"]
    end

    subgraph NUCLI["Nucli de l'aplicacio"]
        PE["Ports d'entrada<br/>GestionarPrestecs"]
        DOM["DOMINI<br/>Prestec, Material,<br/>CalculadoraMultes"]
        PS["Ports de sortida<br/>RepositoriPrestecs<br/>PassarelaMetadades<br/>NotificadorAvisos"]
        PE --> DOM
        DOM --> PS
    end

    subgraph EXT_DRE["Adaptadors de sortida"]
        JPA["JPA / PostgreSQL"]
        HTTP["HttpClient"]
        MAIL["SMTP"]
    end

    REST --> PE
    CLI --> PE
    SOCK --> PE
    PS -.-> JPA
    PS -.-> HTTP
    PS -.-> MAIL

    style DOM fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

A BiblioTech, els ports ja existeixen gairebé tots, només que dispersos i sense nom:

Port Tipus Adaptador actual Altre adaptador possible
GestionarPrestecs Entrada PrestecController (12-04) OrdrePrestec de la CLI (12-03)
ConsultarCataleg Entrada ServidorCataleg (mòdul 9) REST, CLI
RepositoriPrestecs Sortida RepositoriPrestecsJpa En memòria, per a proves
PassarelaMetadades Sortida ClientMetadades (HttpClient) Fitxer local, WireMock
NotificadorAvisos Sortida ServeiAvisos per correu Consola, cua de missatges

El benefici es veu en provar. Amb ports, una prova del cas d'ús complet no necessita ni base de dades ni xarxa:

@Test
void retornarAmbRetardGeneraMulta() {
    var repositori = new RepositoriPrestecsEnMemoria();   // adaptador de prova
    var notificador  = new NotificadorSilencios();
    var rellotge = Clock.fixed(Instant.parse("2026-03-20T10:00:00Z"), ZoneId.of("Europe/Madrid"));
    var gestor = new GestorPrestecs(repositori, notificador, rellotge);

    repositori.guardar(unPrestecDe("978-0000000001", "Marta Ruiz")
        .ambVenciment(LocalDate.of(2026, 3, 10)));

    Diner multa = gestor.retornar(1L).multa();

    assertThat(multa).isEqualTo(Diner.euros("5.00"));   // 10 dies x 0,50 EUR
}

Sense @SpringBootTest, sense H2, sense @DataJpaTest. Mil·lisegons. Això és el que compra l'arquitectura hexagonal.

  1. Comparativa: capes enfront d'hexagonal

Aspecte Per capes Hexagonal (ports i adaptadors)
Metàfora Pila horitzontal Nucli envoltat d'endolls
Direcció de dependències De dalt a baix (i la infraestructura al domini, si s'inverteix) Sempre cap al nucli
On es defineixen les interfícies de persistència Sovint a la capa d'infraestructura Sempre al nucli
Nombre d'interfícies Menor Major (un port per necessitat externa)
Proves del nucli sense infraestructura Possible amb esforç Natural
Canviar de base de dades o de framework Costós si hi va haver filtracions Escriure un adaptador nou
Corba d'aprenentatge Baixa, tothom la coneix Mitjana
Risc Que les capes se saltin Sobreenginyeria: ports per a tot
Encaixa bé en CRUD, aplicacions petites o mitjanes Sistemes amb regles de negoci riques i vida llarga

El que no has de concloure: que l'hexagonal és «millor». Un CRUD de 8 entitats amb hexagonal complet té tres vegades més fitxers i zero avantatge. I són compatibles: l'hexagonal és, a la pràctica, arquitectura per capes amb la regla de dependència aplicada sense excepcions i les interfícies col·locades al costat correcte.

BiblioTech farà servir capes amb inversió de dependències a les fronteres externes, que és hexagonal lleugera: ports on hi ha tecnologia externa (persistència, HTTP, notificacions), crida directa on no n'hi ha.

  1. Organització de paquets: per capa enfront de per funcionalitat

Decidida l'arquitectura, queda decidir com es tradueix a paquets. Hi ha dues escoles.

Per capa (layer-first): el primer nivell de paquets és la capa.

com.nexussoftware.bibliotech
├── controlador
├── servei
├── domini
└── repositori

Per funcionalitat (feature-first, o package by feature): el primer nivell és l'àrea de negoci.

com.nexussoftware.bibliotech
├── cataleg
├── prestecs
├── reserves
└── empleats
Criteri Per capa Per funcionalitat
En afegir una funcionalitat Toques 4 paquets llunyans Toques 1 paquet
En llegir el projecte per primera vegada Veus la tecnologia Veus el negoci
Cohesió Baixa: servei barreja préstecs i catàleg Alta
Acoblament visible Ocult Evident (els import creuats salten a la vista)
Ús de visibilitat de paquet (package-private) Gairebé impossible Molt efectiu: pots amagar classes internes de la funcionalitat
Esborrar una funcionalitat Arqueologia Esborrar un directori
Escala a 50 classes Acceptable
Escala a 500 classes Malament

L'argument decisiu és el de la visibilitat. Amb paquets per funcionalitat pots escriure:

package com.nexussoftware.bibliotech.prestecs;

// package-private: NINGU de fora de la funcionalitat "prestecs" pot tocar aixo.
class CalculadoraMultes { ... }

Amb paquets per capa, CalculadoraMultes és a servei al costat d'altres vint classes i ha de ser public perquè la faci servir qui la necessita: és a dir, pública per a tot el projecte. La capacitat d'amagar és la que impedeix que les fronteres s'erosionin.

  1. L'arbre de paquets de BiblioTech, en les dues opcions

Opció A — per capa:

src/main/java/com/nexussoftware/bibliotech/
├── BiblioTechApplication.java
├── controlador/
│   ├── PrestecController.java
│   ├── CatalegController.java
│   └── ReservaController.java
├── servei/
│   ├── GestorPrestecs.java
│   ├── ProcessadorReserves.java
│   ├── ServeiAvisos.java
│   ├── EstadistiquesBiblioTech.java
│   └── EnriquidorCataleg.java
├── domini/
│   ├── Material.java
│   ├── Llibre.java
│   ├── Revista.java
│   ├── Dvd.java
│   ├── Prestec.java
│   ├── Reserva.java
│   ├── Empleat.java
│   ├── EstatPrestec.java
│   └── Gravetat.java
├── repositori/
│   ├── PrestecRepository.java
│   ├── MaterialRepository.java
│   └── ReservaRepository.java
└── dto/
    ├── FitxaMaterialDto.java
    └── CrearPrestecDto.java

Opció B — per funcionalitat:

src/main/java/com/nexussoftware/bibliotech/
├── BiblioTechApplication.java
├── compartit/                        ← nomes el genuinament transversal
│   ├── Diner.java
│   ├── Isbn.java
│   ├── Gravetat.java
│   └── BiblioTechException.java
├── cataleg/
│   ├── Material.java                 (package-private on es pot)
│   ├── Llibre.java
│   ├── Revista.java
│   ├── Dvd.java
│   ├── TipusMaterial.java
│   ├── CatalegService.java           ← API publica de la funcionalitat
│   ├── CatalegController.java
│   ├── MaterialRepository.java
│   └── metadades/
│       ├── PassarelaMetadades.java   (port)
│       └── ClientMetadades.java      (adaptador HTTP)
├── prestecs/
│   ├── Prestec.java
│   ├── EstatPrestec.java
│   ├── CalculadoraMultes.java        (package-private)
│   ├── GestorPrestecs.java           ← API publica
│   ├── PrestecController.java
│   └── PrestecRepository.java
├── reserves/
│   ├── Reserva.java
│   ├── ProcessadorReserves.java
│   └── ReservaRepository.java
├── empleats/
│   ├── Empleat.java
│   └── EmpleatRepository.java
└── avisos/
    ├── NotificadorAvisos.java        (port)
    └── ServeiAvisos.java             (adaptador)

  1. Recomanació justificada

Per a BiblioTech: per funcionalitat al primer nivell, per capa dins de cada funcionalitat. És a dir, l'opció B.

Les raons, en ordre de pes:

  1. El projecte creixerà. El mòdul 12 hi afegeix CLI, web, seguretat i observabilitat. Per capa, el paquet servei acabaria amb vint classes sense relació entre si.
  2. Permet amagar. CalculadoraMultes és un detall de com funcionen els préstecs; ningú més no hauria de poder cridar-la. Només el paquet per funcionalitat ho fa complir.
  3. El codi es llegeix per negoci, no per tecnologia. Quan la Núria Vidal pregunta «on és la regla de les reserves caducades?», la resposta és reserves/, no «busca a servei, després a domini, després a repositori».
  4. Els canvis són locals. Afegir «renovació de préstec» toca prestecs/ i res més.
  5. Fa visible l'acoblament. Si reserves necessita cinc classes de prestecs, els import ho criden. Amb paquets per capa, aquest mateix acoblament és invisible.

Un avís honest: l'opció B té un punt feble, el paquet compartit. Tendeix a convertir-se en un calaix de sastre. La regla és: una cosa entra a compartit només si la fan servir tres funcionalitats o més i no pertany a cap. Diner i Isbn sí; CalculadoraMultes no, encara que la reutilitzin dues.

  1. Projecte multimòdul Maven aplicat a BiblioTech

Els paquets són una convenció: el compilador no impedeix que domini importi controlador. Els mòduls Maven sí que ho impedeixen, perquè un mòdul només veu el que declara com a dependència, i Maven prohibeix els cicles.

Aquesta és l'estructura que tindrà BiblioTech:

flowchart BT
    DOM["bibliotech-domini<br/>sense dependencies externes"]
    APP["bibliotech-aplicacio<br/>casos d'us"]
    INF["bibliotech-infraestructura<br/>JPA, HTTP, Spring"]
    CON["bibliotech-consola<br/>Picocli"]
    WEB["bibliotech-web<br/>Spring MVC"]

    APP --> DOM
    INF --> DOM
    INF --> APP
    CON --> APP
    CON --> INF
    WEB --> APP
    WEB --> INF

    style DOM fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px

Responsabilitat i dependències permeses de cada mòdul:

Mòdul Conté Depèn de Dependències externes permeses
bibliotech-domini Entitats, objectes de valor, regles, ports, excepcions Res Cap (com a màxim, l'API de validació)
bibliotech-aplicacio Casos d'ús, orquestració, DTO interns domini Cap, o només anotacions de transacció
bibliotech-infraestructura Adaptadors JPA, HTTP, correu, configuració de Spring domini, aplicació Spring, Hibernate, Jackson, HttpClient
bibliotech-consola Ordres Picocli, formatadors aplicació, infraestructura Picocli, Spring Boot
bibliotech-web Controladors REST, DTO d'API, gestor global d'errors aplicació, infraestructura Spring Web, validació, springdoc

L'estructura de directoris resultant:

bibliotech/
├── pom.xml                       ← POM pare (packaging: pom)
├── mvnw / mvnw.cmd / .mvn/
├── bibliotech-domini/
│   ├── pom.xml
│   └── src/{main,test}/java/...
├── bibliotech-aplicacio/
│   ├── pom.xml
│   └── src/{main,test}/java/...
├── bibliotech-infraestructura/
│   ├── pom.xml
│   └── src/{main,test}/{java,resources}/...
├── bibliotech-consola/
│   ├── pom.xml
│   └── src/{main,test}/{java,resources}/...
└── bibliotech-web/
    ├── pom.xml
    └── src/{main,test}/{java,resources}/...

  1. El POM pare i dependencyManagement

El POM pare no produeix codi: agrega els mòduls i centralitza les versions. Recorda de 11-05 que dependencyManagement declara versions sense afegir dependències; els fills les hereten i només escriuen groupId i artifactId.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <!-- Heretem de Spring Boot: ens dona el BOM amb les versions compatibles
       de mes de 400 llibreries, i la configuracio per defecte dels plugins. -->
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.4</version>
    <relativePath/>
  </parent>

  <groupId>com.nexussoftware</groupId>
  <artifactId>bibliotech</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>            <!-- clau: agregador, no produeix jar -->
  <name>BiblioTech</name>
  <description>Gestio de la biblioteca tecnica interna de Nexus Software</description>

  <!-- L'ordre aqui es irrellevant: Maven ordena els moduls per les seves dependencies. -->
  <modules>
    <module>bibliotech-domini</module>
    <module>bibliotech-aplicacio</module>
    <module>bibliotech-infraestructura</module>
    <module>bibliotech-consola</module>
    <module>bibliotech-web</module>
  </modules>

  <properties>
    <java.version>21</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <picocli.version>4.7.6</picocli.version>
    <mapstruct.version>1.6.2</mapstruct.version>
    <springdoc.version>2.6.0</springdoc.version>
  </properties>

  <dependencyManagement>
    <dependencies>
      <!-- Els nostres propis moduls: els fills els faran servir sense repetir la versio. -->
      <dependency>
        <groupId>com.nexussoftware</groupId>
        <artifactId>bibliotech-domini</artifactId>
        <version>${project.version}</version>
      </dependency>
      <dependency>
        <groupId>com.nexussoftware</groupId>
        <artifactId>bibliotech-aplicacio</artifactId>
        <version>${project.version}</version>
      </dependency>
      <dependency>
        <groupId>com.nexussoftware</groupId>
        <artifactId>bibliotech-infraestructura</artifactId>
        <version>${project.version}</version>
      </dependency>

      <!-- Externes que Spring Boot no gestiona -->
      <dependency>
        <groupId>info.picocli</groupId>
        <artifactId>picocli-spring-boot-starter</artifactId>
        <version>${picocli.version}</version>
      </dependency>
      <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
        <version>${springdoc.version}</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <!-- Aquestes si que les hereta TOTHOM, perque tothom fa proves. -->
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

El POM del domini és on l'arquitectura es torna verificable per l'eina:

<project ...>
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>com.nexussoftware</groupId>
    <artifactId>bibliotech</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>

  <artifactId>bibliotech-domini</artifactId>
  <name>BiblioTech :: Domini</name>

  <!-- Fixa-t'hi be: NO HI HA <dependencies> de produccio.
       Ni Spring, ni Hibernate, ni Jackson, ni HttpClient de tercers.
       Nomes la biblioteca estandard de Java 21.
       Aixo no es un adorn: es la regla de dependencia, feta complir per Maven. -->
</project>

I el d'infraestructura, que sí que pot portar tecnologia:

<project ...>
  <parent>
    <groupId>com.nexussoftware</groupId>
    <artifactId>bibliotech</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>

  <artifactId>bibliotech-infraestructura</artifactId>
  <name>BiblioTech :: Infraestructura</name>

  <dependencies>
    <dependency>
      <groupId>com.nexussoftware</groupId>
      <artifactId>bibliotech-aplicacio</artifactId>   <!-- versio heretada del pare -->
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
      <groupId>org.flywaydb</groupId>
      <artifactId>flyway-core</artifactId>
    </dependency>
    <dependency>
      <groupId>org.postgresql</groupId>
      <artifactId>postgresql</artifactId>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</project>

Construcció i comprovació:

# Construeix els cinc moduls en l'ordre correcte (Maven ho dedueix del graf)
./mvnw clean install

# Nomes el domini i el que necessita per compilar (-am = also make)
./mvnw -pl bibliotech-domini -am test

# L'arbre de dependencies del domini: ha de ser practicament buit
./mvnw -pl bibliotech-domini dependency:tree

  1. Com el graf de mòduls impedeix que el domini importi Spring

Aquesta és la part que converteix una recomanació en una garantia. Suposa que en Diego Alonso, amb pressa, escriu això al mòdul de domini:

package com.nexussoftware.bibliotech.domini.prestecs;

import org.springframework.stereotype.Service;   // dins de bibliotech-domini

@Service
public class CalculadoraMultes { ... }

Resultat:

$ ./mvnw -pl bibliotech-domini compile
[ERROR] /.../CalculadoraMultes.java:[3,32] package org.springframework.stereotype does not exist
[ERROR] /.../CalculadoraMultes.java:[5,2] cannot find symbol: class Service
[INFO] BUILD FAILURE

No és una convenció que algú hagi de recordar en la revisió de codi: és un error de compilació. I el mateix passa en l'altra direcció: si algú intenta que el domini depengui d'infraestructura per «arreglar-ho», Maven detecta el cicle:

[ERROR] The projects in the reactor contain a cyclic reference:
        bibliotech-domini -> bibliotech-infraestructura -> bibliotech-domini

Aquest és l'argument definitiu a favor del multimòdul, i mereix enunciar-se com a principi general:

Les regles que depenen de la disciplina humana es trenquen. Les que depenen de l'eina, no.

La mateixa idea que el lombok.config d'11-07 (convertir «no facis servir @Data en entitats» en una cosa que el compilador verifica) aplicada a l'arquitectura sencera.

Si per alguna raó no pots anar a multimòdul, l'alternativa és ArchUnit (esmentada a 11-07), que expressa les mateixes regles com a proves JUnit:

@AnalyzeClasses(packages = "com.nexussoftware.bibliotech")
class ReglesArquitecturaTest {

    @ArchTest
    static final ArchRule elDominiNoDepenDeSpring =
        noClasses().that().resideInAPackage("..domini..")
            .should().dependOnClassesThat().resideInAnyPackage("org.springframework..");

    @ArchTest
    static final ArchRule elDominiNoDepenDeJpa =
        noClasses().that().resideInAPackage("..domini..")
            .should().dependOnClassesThat().resideInAnyPackage("jakarta.persistence..");

    @ArchTest
    static final ArchRule senseCicles =
        slices().matching("com.nexussoftware.bibliotech.(*)..").should().beFreeOfCycles();
}

És pitjor que el multimòdul (la regla es comprova en fase de proves, no de compilació), però és infinitament millor que res, i es pot aplicar avui a qualsevol projecte sense reestructurar-lo.

  1. DTO enfront d'entitats

Ara una decisió que sembla menor i arruïna projectes: pot un controlador REST retornar directament una entitat JPA?

Tècnicament sí. Jackson serialitza Prestec sense protestar. I és un error, per sis raons concretes:

Problema Què passa exactament
Acoblament del contracte a l'esquema Reanomenes la columna data_venc a data_venciment i trenques tots els clients de l'API
Fuita de dades L'entitat EmpleathashContrasenya, dni, salari. Tot això viatja al JSON
Càrrega mandrosa Jackson accedeix a prestec.getMaterial() fora de la transacció: LazyInitializationException, o pitjor, N+1 (11-03)
Cicles infinits PrestecEmpleatList<Prestec>StackOverflowError
Entrada perillosa Amb @RequestBody Prestec, un client pot enviar {"id": 7, "version": 3, "estat": "RETORNAT"} i modificar camps que no hauria de tocar
Formats diferents L'API vol LocalDate en ISO-8601 i un camp calculat diesRestants; l'entitat no té per què

La solució és un DTO (Data Transfer Object): un objecte l'única feina del qual és creuar la frontera. I a Java 21, un DTO és un record (04-07):

package com.nexussoftware.bibliotech.web.dto;

/**
 * El que l'API RETORNA d'un prestec. Contracte public, estable,
 * independent de com estigui modelada l'entitat per dins.
 */
public record PrestecResponse(
        Long id,
        String titolMaterial,
        String isbn,
        String nomEmpleat,
        LocalDate dataPrestec,
        LocalDate dataVenciment,
        LocalDate dataDevolucio,   // null si encara esta prestat: JSON no te Optional
        String estat,
        long diesRestants,         // camp calculat que l'entitat no te
        BigDecimal multa) {

    public static PrestecResponse desDe(Prestec p, LocalDate avui) {
        return new PrestecResponse(
            p.getId(),
            p.getMaterial().getTitol(),
            p.getMaterial().getIsbn().valor(),
            p.getEmpleat().getNom(),
            p.getDataPrestec(),
            p.getDataVenciment(),
            p.getDataDevolucio().orElse(null),
            p.getEstat().name(),
            ChronoUnit.DAYS.between(avui, p.getDataVenciment()),
            p.multaAcumulada(avui).quantitat());
    }
}
/**
 * El que l'API ACCEPTA per crear un prestec. Nomes els camps que
 * el client te dret a decidir. Ni id, ni version, ni estat.
 */
public record CrearPrestecRequest(
        @NotBlank @Isbn String isbn,
        @NotNull Long idEmpleat,
        @Positive @Max(30) Integer dies) {
}

Fixa't en el que fa l'asimetria entre els dos records: el client no pot enviar id, version ni estat, perquè l'objecte on es deserialitza no té aquests components. La seguretat no depèn que el servidor recordi ignorar-los.

Sobre el mapatge: escriure desDe(...) a mà és perfectament acceptable i és el que farem, perquè és explícit i no hi afegeix màgia. Quan els DTO es multipliquen, MapStruct (esmentat a 11-07) genera aquests mapejadors en compilació a partir d'una interfície anotada, sense reflexió i sense cost en execució:

@Mapper(componentModel = "spring")
public interface PrestecMapper {
    @Mapping(target = "titolMaterial", source = "material.titol")
    @Mapping(target = "nomEmpleat", source = "empleat.nom")
    PrestecResponse aResponse(Prestec prestec);
}

Regla pràctica: entitat JPA cap endins, DTO cap enfora. La frontera de l'aplicació (REST, CLI, missatges) no veu mai una entitat.

  1. Configuració per entorn: application.yml i perfils

BiblioTech ja té perfils dev i prod des d'11-02. Ara cal organitzar-los bé, amb una regla de fons:

El mateix artefacte es desplega a tots els entorns. El que canvia és la configuració, mai el jar.

Fitxer base, bibliotech-web/src/main/resources/application.yml:

spring:
  application:
    name: bibliotech
  jpa:
    open-in-view: false            # desactiva-ho SEMPRE: evita consultes a la capa de vista (11-03)
    properties:
      hibernate:
        jdbc.batch_size: 25
  threads:
    virtual:
      enabled: true                # fils virtuals de Java 21 (10-06), s'explica a 12-04

server:
  port: 8080
  shutdown: graceful               # aturada ordenada, es desenvolupa a 12-06

bibliotech:                        # les nostres propietats, tipades a PropietatsBiblioTech
  prestec:
    dies-per-defecte: 15
    maxim-per-empleat: 3
  multa:
    euros-per-dia: 0.50
    maxima: 20.00
  metadades:
    url: https://api.metadades.exemple/v1
    temps-espera: 3s

logging:
  level:
    com.nexussoftware.bibliotech: INFO

Perfil de desenvolupament, application-dev.yml:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/bibliotech
    username: bibliotech
    password: bibliotech            # local, exhaurible, mai reutilitzada fora
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: validate            # mai 'update': l'esquema el governa Flyway (12-06)
  flyway:
    enabled: true

logging:
  level:
    com.nexussoftware.bibliotech: DEBUG
    org.hibernate.SQL: DEBUG

Perfil de producció, application-prod.yml:

spring:
  datasource:
    url: ${BIBLIOTECH_DB_URL}       # sense valor per defecte: si falta, l'app NO arrenca
    username: ${BIBLIOTECH_DB_USER}
    password: ${BIBLIOTECH_DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate

server:
  error:
    include-stacktrace: never       # mai filtrar traces al client (12-04, 12-07)
    include-message: never

logging:
  level:
    root: WARN
    com.nexussoftware.bibliotech: INFO

Que ${BIBLIOTECH_DB_URL} no tingui valor per defecte és intencionat: si la variable no està definida, l'aplicació falla en arrencar amb un missatge clar. És infinitament preferible a arrencar en producció apuntant silenciosament a una base de dades de proves.

Activació del perfil:

# En desenvolupament
./mvnw -pl bibliotech-web spring-boot:run -Dspring-boot.run.profiles=dev

# En produccio (variable d'entorn, no argument)
SPRING_PROFILES_ACTIVE=prod java -jar bibliotech-web.jar

  1. La jerarquia de fonts de configuració de Spring Boot

Spring Boot llegeix la configuració de molts llocs i resol els conflictes per precedència. Aquesta taula, de major a menor prioritat (versió abreujada de l'oficial, amb el que es fa servir de debò), és de les que convé tenir a mà:

# Font Exemple Ús típic
1 Arguments de línia d'ordres --server.port=9090 Ajust puntual, depuració
2 Propietats de sistema de la JVM -Dserver.port=9090 Arrencada des d'scripts
3 Variables d'entorn SERVER_PORT=9090 Producció i contenidors
4 application-{perfil}.yml extern (al costat del jar) ./config/application-prod.yml Configuració de l'operador
5 application-{perfil}.yml empaquetat application-prod.yml Diferències per entorn
6 application.yml extern ./application.yml Sobreescriptura de l'operador
7 application.yml empaquetat application.yml Valors base
8 @PropertySource Casos heretats
9 Valors per defecte al codi @Value("${x:10}") Últim recurs

Dues conseqüències pràctiques:

  • La relaxació de noms: bibliotech.multa.euros-per-dia es pot fixar amb la variable d'entorn BIBLIOTECH_MULTA_EUROSPERDIA. Majúscules, punts i guions a subratllat. Aquesta correspondència és el que fa possible configurar qualsevol cosa en un contenidor sense tocar fitxers.
  • Depurar la configuració: l'endpoint /actuator/env (protegit, com veurem a 12-07) mostra el valor efectiu de cada propietat i de quina font va venir. És la resposta a «juraria que vaig posar el port 9090».

  1. Variables d'entorn i secrets fora del repositori

AVÍS IMPORTANT. Cap credencial, clau d'API, certificat, contrasenya de base de dades, testimoni ni secret de signatura no pot ser al repositori. Mai. Ni a application.yml, ni en un .properties «només de proves», ni en un comentari, ni en una prova. Git recorda per sempre: esborrar-ho en un commit posterior no ho elimina de l'historial, i si el repositori ha estat clonat o publicat, el secret ja està compromès i cal rotar-lo, no amagar-lo.

Els mecanismes, de menys a més seriós:

Mecanisme Quan Compte
Variables d'entorn Gairebé sempre; estàndard de facto en contenidors Visibles a /proc i en bolcats d'entorn
Fitxer .env local, ignorat per Git Desenvolupament Que sigui a .gitignore abans de crear-lo
Fitxer extern muntat en el desplegament Servidors propis Permisos 600 i propietari correcte
Gestor de secrets (Vault, AWS Secrets Manager, Secrets de Kubernetes) Producció seriosa Es desenvolupa a 12-07

Detecció primerenca, que és el que de debò evita l'incident:

# Cercar secrets a TOT l'historial, no nomes a l'arbre actual
gitleaks detect --source . --verbose

# Com a enganxall de pre-commit, perque no hi arribi ni a entrar
pre-commit run --all-files

A 12-07 es reprèn aquest tema amb la gestió completa: rotació, xifratge en repòs i què cal fer quan ja ha passat.

  1. Control de versions: .gitignore, branques i commits convencionals

El .gitignore, a l'arrel del projecte multimòdul:

# --- Maven ---
target/
!.mvn/wrapper/maven-wrapper.jar
.mvn/timing.properties
dependency-reduced-pom.xml

# --- Java ---
*.class
*.jar
*.war
hs_err_pid*.log
replay_pid*.log

# --- IDE: IntelliJ ---
.idea/
*.iml
*.iws

# --- IDE: Eclipse ---
.classpath
.project
.settings/
bin/

# --- IDE: VS Code (es versiona launch.json si l'equip el comparteix) ---
.vscode/*
!.vscode/launch.json
!.vscode/settings.json

# --- Sistema operatiu ---
.DS_Store
Thumbs.db

# --- Local i secrets ---
.env
*.local.yml
application-local.yml
/config/secrets/
*.p12
*.jks
*.pem

Les dues últimes seccions són les importants. target/ el té tothom; els .p12 i els .env són els que causen disgustos.

Estratègia de branques. Per a un equip petit com el de Nexus Software, trunk-based amb branques curtes:

Branca Viu Propòsit
main Sempre Sempre desplegable. Protegida: ningú no hi empeny directament
feature/xxx Hores o pocs dies Una funcionalitat. S'integra per Pull Request amb CI en verd
fix/xxx Hores Correcció
release/x.y Només si hi ha versions suportades en paral·lel Manteniment d'una versió antiga

Branques de vida llarga = conflictes d'integració grans. La regla és: si una branca porta més de tres dies oberta, la feina estava mal trossejada.

Missatges de commit convencionals (Conventional Commits). No és burocràcia: permet generar el registre de canvis i deduir la versió semàntica automàticament.

Format: tipus(àmbit): descripció en imperatiu

Tipus Significat Exemple real de BiblioTech
feat Funcionalitat nova feat(prestecs): permetre renovar un prestec un cop
fix Correcció d'error fix(multes): no cobrar els dies de tancament per vacances
refactor Canvi intern sense canvi de comportament refactor(cataleg): extreure PassarelaMetadades com a port
test Proves test(prestecs): cobrir el limit de 3 prestecs actius
docs Documentació docs(readme): afegir instruccions d'arrencada amb Docker
build Construcció i dependències build(deps): pujar Spring Boot a 3.3.4
ci Integració contínua ci: publicar informe de cobertura al PR
perf Rendiment perf(cataleg): evitar N+1 en llistar materials
chore Tasques diverses chore: actualitzar .gitignore

Un canvi incompatible es marca amb ! o amb un peu BREAKING CHANGE:, i això és el que dispara una pujada de versió major:

feat(api)!: eliminar el camp `disponible` de MaterialResponse

BREAKING CHANGE: els clients han de fer servir `unitatsDisponibles`, que es un
enter, en lloc del booleà `disponible`. El camp antic va estar marcat
com a obsolet des de la versió 1.4.0.

  1. Format i estil: .editorconfig i Spotless

Discutir claus i sagnats en una revisió de codi és temps llençat. S'automatitza i s'oblida.

.editorconfig — l'entenen gairebé tots els editors, sense plugins:

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
max_line_length = 120

[*.{xml,yml,yaml,json}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false   # dos espais finals = salt de linia a Markdown

[*.{sh,bash}]
indent_size = 2

Spotless — formata de debò, i fa fallar la construcció si el codi no està formatat. Al POM pare:

<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>com.diffplug.spotless</groupId>
        <artifactId>spotless-maven-plugin</artifactId>
        <version>2.43.0</version>
        <configuration>
          <java>
            <palantirJavaFormat/>          <!-- o <googleJavaFormat/> -->
            <removeUnusedImports/>
            <importOrder>
              <order>java,javax,jakarta,org,com,com.nexussoftware,</order>
            </importOrder>
            <licenseHeader>
              <content>/* BiblioTech - Nexus Software */</content>
            </licenseHeader>
          </java>
          <pom>
            <sortPom/>
          </pom>
        </configuration>
        <executions>
          <execution>
            <goals>
              <!-- 'check' fa fallar la construccio; 'apply' ho arregla.
                   A CI volem que falli. -->
              <goal>check</goal>
            </goals>
            <phase>validate</phase>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </pluginManagement>
</build>
./mvnw spotless:apply    # formata tot el projecte
./mvnw spotless:check    # nomes comprova: aixo es el que corre a CI

Un consell de procés: fes la reformatació massiva inicial en un commit propi, que no contingui cap canvi funcional, i anota'l a .git-blame-ignore-revs perquè no embruti el git blame:

# .git-blame-ignore-revs
# Reformatacio inicial amb Spotless (sense canvis funcionals)
a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0
git config blame.ignoreRevsFile .git-blame-ignore-revs

  1. Un README.md que serveixi de debò

Gairebé tots els README són inútils perquè expliquen el que el projecte és i no el que el lector necessita fer. El criteri de qualitat és objectiu:

Una persona que no ha vist mai el projecte, només amb el README, ha de poder arrencar-lo i executar les proves en menys de deu minuts.

# BiblioTech

Sistema de gestió de la biblioteca tècnica interna de Nexus Software.
Gestiona el catàleg de materials (llibres, revistes, DVD), els préstecs
a empleats, les reserves i les multes per retard.

## Requisits

| Eina | Versió | Comprovació |
|---|---|---|
| JDK | 21+ | `java -version` |
| Docker | 24+ | `docker --version` |
| Maven | No cal: fes servir `./mvnw` | — |

## Arrencada ràpida

git clone https://git.nexussoftware.com/bibliotech.git cd bibliotech docker compose up -d # PostgreSQL a localhost:5432 ./mvnw clean install ./mvnw -pl bibliotech-web spring-boot:run -Dspring-boot.run.profiles=dev

Comprovació:

curl http://localhost:8080/actuator/health # {"status":"UP"} curl http://localhost:8080/api/materials

Documentació de l'API: <http://localhost:8080/swagger-ui.html>

## Ordres habituals

| Objectiu | Ordre |
|---|---|
| Compilar-ho tot | `./mvnw clean install` |
| Només proves unitàries | `./mvnw test` |
| Proves d'integració | `./mvnw verify` |
| Informe de cobertura | `./mvnw verify` → `target/site/jacoco/index.html` |
| Formatar el codi | `./mvnw spotless:apply` |
| CLI | `java -jar bibliotech-consola/target/bibliotech-consola.jar cataleg llistar` |

## Estructura

| Mòdul | Responsabilitat |
|---|---|
| `bibliotech-domini` | Entitats i regles de negoci. Sense dependències externes |
| `bibliotech-aplicacio` | Casos d'ús |
| `bibliotech-infraestructura` | JPA, HTTP, correu, configuració de Spring |
| `bibliotech-consola` | CLI amb Picocli |
| `bibliotech-web` | API REST |

Decisions d'arquitectura: [`docs/adr/`](docs/adr/).

## Configuració

Totes les propietats pròpies estan sota `bibliotech.*` a `application.yml`.
En producció s'injecten per variables d'entorn (vegeu `docs/desplegament.md`).
**Mai** no s'afegeixen secrets al repositori.

## Contribuir

1. Branca des de `main`: `feature/descripcio-curta`
2. Commits amb [Conventional Commits](https://www.conventionalcommits.org/)
3. `./mvnw verify` i `./mvnw spotless:check` en verd
4. Pull Request; requereix una aprovació i CI en verd

El que no ha de portar un README: el diagrama de classes complet (queda obsolet en una setmana), la història del projecte, ni la documentació de l'API (aquesta la genera OpenAPI).

  1. Registre de decisions d'arquitectura (ADR)

Nota: ADR (Architecture Decision Record). Un ADR és un fitxer Markdown curt, numerat i immutable, que registra una decisió d'arquitectura: el context, la decisió i les seves conseqüències. Viu a docs/adr/ dins del repositori, al costat del codi que descriu. No s'edita quan la decisió canvia: s'escriu un ADR nou que substitueix l'anterior. El seu valor no és en el present, sinó d'aquí a dos anys, quan algú pregunti «per què dimonis això està així?» i l'alternativa sigui endevinar.

Exemple real, extret d'una decisió que ja vam prendre al mòdul 11:

# ADR-004: Renunciar a `sealed` a la jerarquia Material

- **Estat:** Acceptada
- **Data:** 2026-06-18
- **Decisors:** Marta Ruiz, Diego Alonso

## Context

`Material` era una interfície `sealed` (Java 17, mòdul 10) amb `Llibre`, `Revista`
i `Dvd` com a úniques implementacions permeses, cosa que habilitava pattern
matching exhaustiu sense `default`.

En passar a JPA amb estratègia `SINGLE_TABLE`, Hibernate necessita crear proxies
per subclasse i les entitats no poden pertànyer a una jerarquia segellada
gestionada d'aquesta manera.

## Decisió

Convertir `Material` en classe abstracta no segellada, amb `@Inheritance(SINGLE_TABLE)`
i `@DiscriminatorColumn(name = "tipus")`.

## Conseqüències

**Positives:** persistència polimòrfica amb una sola consulta; sense JOIN per tipus.

**Negatives:** es perd l'exhaustivitat del `switch`; cal mantenir una branca
`default` que llanci `IllegalStateException`. Qualsevol pot heretar de `Material`
sense que el compilador ho impedeixi.

**Mitigació:** una prova d'ArchUnit comprova que les subclasses de `Material`
són exactament tres.

## Alternatives descartades

- **`TABLE_PER_CLASS`:** mantindria el model, però les consultes polimòrfiques
  es converteixen en `UNION` i el rendiment del catàleg es degrada.
- **Model de domini separat del de persistència:** conserva `sealed`, a canvi
  de duplicar nou classes i els seus mapatges. Desproporcionat per a aquest projecte.

Escriu un ADR quan la decisió sigui cara de revertir: elecció de base de dades, de framework, estructura de mòduls, estratègia d'autenticació, format de l'API. No n'escriguis un per triar el nom d'una variable.

  1. Scripts d'arrencada i dependències locals

L'objectiu és que arrencar l'entorn de desenvolupament sigui una ordre. Per a les dependències (base de dades, i més endavant el que calgui), Docker Compose:

# compose.yaml -- nomes dependencies, NO l'aplicacio.
# En desenvolupament l'app corre a l'IDE, amb recarrega en calent i depurador.
services:
  postgres:
    image: postgres:16-alpine
    container_name: bibliotech-db
    environment:
      POSTGRES_DB: bibliotech
      POSTGRES_USER: bibliotech
      POSTGRES_PASSWORD: bibliotech
    ports:
      - "5432:5432"
    volumes:
      - bibliotech-dades:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U bibliotech"]
      interval: 5s
      timeout: 3s
      retries: 10

volumes:
  bibliotech-dades:
docker compose up -d        # aixecar
docker compose logs -f      # veure registres
docker compose down         # parar (les dades sobreviuen al volum)
docker compose down -v      # parar i ESBORRAR les dades

I un script de conveniència, scripts/dev.sh:

#!/usr/bin/env bash
set -euo pipefail          # -e: falla al primer error; -u: variable sense definir es error;
                           # -o pipefail: una fallada al mig d'una canonada compta

cd "$(dirname "$0")/.."    # executable des de qualsevol directori

echo "==> Aixecant dependencies"
docker compose up -d --wait     # --wait espera que el healthcheck estigui en verd

echo "==> Construint"
./mvnw -q clean install -DskipTests

echo "==> Arrencant BiblioTech (perfil dev)"
exec ./mvnw -pl bibliotech-web spring-boot:run -Dspring-boot.run.profiles=dev

Docker Compose es desenvolupa a fons a 12-06, inclosa l'aplicació contenidoritzada. Aquí només el necessitem per tenir PostgreSQL en local i deixar de fer servir H2 com a base de dades de desenvolupament, que és una font de sorpreses (12-05 explica per què H2 menteix).

  1. L'arbre complet de BiblioTech reestructurat

Aquest és l'estat final del projecte en acabar la lliçó:

bibliotech/
├── .editorconfig                       # estil comu a tots els editors
├── .gitignore
├── .git-blame-ignore-revs              # ignora el commit de reformatacio
├── .github/
│   └── workflows/
│       └── ci.yml                      # s'escriu a 12-05
├── compose.yaml                        # PostgreSQL local
├── mvnw, mvnw.cmd, .mvn/               # wrapper: mateixa versio de Maven per a tothom (11-05)
├── pom.xml                             # POM pare: moduls + dependencyManagement
├── README.md
├── docs/
│   ├── adr/
│   │   ├── 0001-arquitectura-per-capes-amb-ports.md
│   │   ├── 0002-projecte-multimodul-maven.md
│   │   ├── 0003-postgresql-en-lloc-de-h2.md
│   │   └── 0004-renunciar-a-sealed-a-material.md
│   └── desplegament.md                 # 12-06
├── scripts/
│   ├── dev.sh
│   └── bibliotech                      # llancador de la CLI (12-03)
│
├── bibliotech-domini/                  # -- SENSE DEPENDENCIES EXTERNES --
│   ├── pom.xml
│   └── src/main/java/com/nexussoftware/bibliotech/domini/
│       ├── compartit/
│       │   ├── Diner.java
│       │   ├── Isbn.java
│       │   ├── Gravetat.java
│       │   └── BiblioTechException.java        # jerarquia del modul 6
│       ├── cataleg/
│       │   ├── Material.java
│       │   ├── Llibre.java  Revista.java  Dvd.java
│       │   ├── TipusMaterial.java
│       │   └── port/
│       │       ├── RepositoriMaterials.java
│       │       └── PassarelaMetadades.java
│       ├── prestecs/
│       │   ├── Prestec.java
│       │   ├── EstatPrestec.java
│       │   ├── CalculadoraMultes.java
│       │   ├── ReglaTarifa.java                # Estrategia, es formalitza a 12-02
│       │   └── port/RepositoriPrestecs.java
│       ├── reserves/
│       │   ├── Reserva.java
│       │   └── port/RepositoriReserves.java
│       └── empleats/
│           ├── Empleat.java
│           └── port/RepositoriEmpleats.java
│
├── bibliotech-aplicacio/               # -- casos d'us --
│   ├── pom.xml                         # depen NOMES de domini
│   └── src/main/java/com/nexussoftware/bibliotech/aplicacio/
│       ├── prestecs/
│       │   ├── GestionarPrestecs.java          # port d'entrada
│       │   └── GestorPrestecs.java             # implementacio
│       ├── cataleg/
│       │   ├── ConsultarCataleg.java
│       │   ├── CatalegService.java
│       │   └── EnriquidorCataleg.java
│       ├── reserves/ProcessadorReserves.java
│       ├── avisos/ServeiAvisos.java
│       └── estadistiques/EstadistiquesBiblioTech.java
│
├── bibliotech-infraestructura/         # -- adaptadors de sortida --
│   ├── pom.xml
│   └── src/main/
│       ├── java/com/nexussoftware/bibliotech/infraestructura/
│       │   ├── persistencia/
│       │   │   ├── RepositoriPrestecsJpa.java
│       │   │   ├── PrestecSpringDataRepository.java
│       │   │   ├── RepositoriMaterialsJpa.java
│       │   │   └── convertidor/IsbnConverter.java
│       │   ├── metadades/ClientMetadades.java        # HttpClient (modul 9)
│       │   ├── avisos/NotificadorCorreu.java
│       │   ├── sockets/ServidorCataleg.java          # el del modul 9, segueix viu
│       │   └── config/
│       │       ├── PropietatsBiblioTech.java
│       │       ├── ConfiguracioRellotge.java         # Clock injectable (10-05)
│       │       └── AspecteCronometre.java            # AOP (11-02)
│       └── resources/db/migration/                   # Flyway (12-06)
│           ├── V1__esquema_inicial.sql
│           └── V2__indexs_cataleg.sql
│
├── bibliotech-consola/                 # -- adaptador d'entrada: CLI (12-03) --
│   ├── pom.xml
│   └── src/main/java/com/nexussoftware/bibliotech/consola/
│       ├── BiblioTechCli.java
│       └── ordres/…
│
└── bibliotech-web/                     # -- adaptador d'entrada: REST (12-04) --
    ├── pom.xml
    └── src/main/
        ├── java/com/nexussoftware/bibliotech/web/
        │   ├── BiblioTechApplication.java
        │   ├── prestecs/PrestecController.java
        │   ├── cataleg/CatalegController.java
        │   ├── dto/…
        │   └── error/GestorGlobalErrors.java
        └── resources/
            ├── application.yml
            ├── application-dev.yml
            ├── application-prod.yml
            └── logback-spring.xml                    # SLF4J + MDC (11-07)

Comprovació que l'arquitectura se sosté:

$ ./mvnw -pl bibliotech-domini dependency:tree
[INFO] com.nexussoftware:bibliotech-domini:jar:1.0.0-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:3.3.4:test
[INFO] \- org.assertj:assertj-core:jar:3.25.3:test

Ni una sola dependència de producció. El domini de BiblioTech és Java 21 pur: es compila en un segon, es prova en mil·lisegons i sobreviurà a Spring.

Errors Comuns i Consells

1. Reestructurar-ho tot de cop en una branca de dues setmanes. És la manera més eficaç que la reestructuració no arribi mai a main. Fes-ho per passos: primer extreure el domini, verificar, integrar; després l'aplicació; després els adaptadors. Cada pas amb les proves en verd i un commit refactor(...).

2. Confondre «paquets» amb «arquitectura». Reanomenar carpetes no canvia res si domini continua important org.springframework. L'arquitectura és la direcció de les dependències, i fins que no la verifica una eina (mòduls Maven o ArchUnit), només és una intenció.

3. Sobreenginyeria hexagonal. No tot necessita un port. Si CalculadoraMultes és una classe de domini pura que ningú no substituirà, crida-la directament. Els ports són per al que creua la frontera de l'aplicació: persistència, xarxa, fitxers, rellotge, notificacions.

4. Un mòdul commons que ho conté tot. Comença amb Diner i Isbn i acaba amb vint utilitats i una dependència de Spring que contamina el domini sencer. Sigues estricte: només hi entra el que fan servir tres funcionalitats i no pertany a cap.

5. Exposar entitats JPA a l'API «de moment». No hi ha cap «de moment». Tan bon punt un client consumeix aquest JSON, l'esquema de la base de dades s'ha convertit en contracte públic. Crea el DTO des del primer endpoint.

6. Ficar secrets al repositori i esborrar-los després. Git no oblida. Si passa: rota el secret immediatament, i només després neteja l'historial. L'ordre invers no serveix de res.

7. ddl-auto: update en qualsevol entorn que no sigui el teu portàtil. Genera esquemes diferents segons l'ordre d'arrencada, no esborra columnes, no versiona res i no és reproduïble. L'esquema es governa amb Flyway (12-06).

8. open-in-view activat. Està a true per defecte a Spring Boot, i manté la sessió d'Hibernate oberta durant el renderitzat de la resposta. El resultat és N+1 invisible i consultes executant-se a la capa de presentació. Posa'l a false i arregla el que es trenqui: el que es trenca estava malament.

9. No fer servir el wrapper de Maven. Sense ./mvnw, «funciona a la meva màquina» apareix per diferències de versió de Maven. El wrapper es versiona al repositori i es fa servir sempre, també a CI.

10. README desactualitzat. Un README que menteix és pitjor que no tenir-lo. Truc: que CI executi les ordres de l'arrencada ràpida. Si el README menteix, la construcció falla.

Consell final: la millor prova que l'estructura funciona és el temps d'incorporació. Si algú nou pot clonar, arrencar, executar les proves i trobar on viu la regla de les multes en menys de mitja hora, l'estructura és bona. Si no, cap justificació teòrica no ho compensa.

Exercicis

Exercici 1: aplicar la regla de dependència

La classe següent és a bibliotech-domini i no compila després de la reestructuració:

package com.nexussoftware.bibliotech.domini.avisos;

import com.nexussoftware.bibliotech.domini.prestecs.Prestec;
import com.nexussoftware.bibliotech.infraestructura.correu.ServidorSmtp;
import org.springframework.stereotype.Service;

@Service
public class ServeiAvisos {

    private final ServidorSmtp smtp = new ServidorSmtp("smtp.nexussoftware.com", 587);

    public void avisarVenciment(Prestec prestec) {
        String cos = "Hola " + prestec.getEmpleat().getNom()
                   + ", el prestec de \"" + prestec.getMaterial().getTitol()
                   + "\" venc el " + prestec.getDataVenciment() + ".";
        smtp.enviar(prestec.getEmpleat().getCorreu(), "Avis de venciment", cos);
    }
}

Enumera totes les violacions arquitectòniques i reescriu el codi repartit entre els mòduls correctes.

Exercici 2: dissenyar els mòduls d'una funcionalitat nova

Nexus Software vol que BiblioTech emeti un informe mensual d'ús en PDF, amb els materials més prestats, els empleats amb més multes i el percentatge de devolucions amb retard. L'informe es genera amb una llibreria externa (openpdf), es desa al disc i s'envia per correu. Ha de poder llançar-se des de la CLI, des de l'API REST i automàticament el dia 1 de cada mes.

Indica, per a cada peça que creïs: en quin mòdul Maven viu, en quin paquet, si és port o adaptador, i de què depèn. Escriu les signatures de les interfícies clau.

Exercici 3: detectar problemes de configuració

Aquest application.yml és al repositori de BiblioTech. Troba almenys sis problemes i escriu la versió corregida, explicant cada canvi.

spring:
  datasource:
    url: jdbc:postgresql://db-produccio.nexussoftware.com:5432/bibliotech
    username: admin
    password: Nexus2026!
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
server:
  port: 8080
  error:
    include-stacktrace: always
bibliotech:
  metadades:
    api-key: sk-live-9f3a2b1c8d7e6f5a
logging:
  level:
    root: DEBUG

Solucions

Solució 1

Violacions detectades:

# Violació Per què és greu
1 El domini importa infraestructura.correu.ServidorSmtp Trenca la regla de dependència. Amb multimòdul, ni compila (i seria un cicle)
2 El domini importa org.springframework El domini no pot dependre d'un framework
3 new ServidorSmtp(...) dins de la classe Instanciació directa d'infraestructura: impossible de provar sense servidor SMTP
4 Host i port encastats al codi Configuració al codi font; diferent per entorn
5 El text del correu es construeix al domini És format de presentació, no regla de negoci
6 Cadena d'accessos prestec.getEmpleat().getCorreu() Llei de Demeter (03-07, es formalitza a 12-02)

Reescriptura. Primer, el port, al domini:

// bibliotech-domini/…/domini/avisos/port/NotificadorAvisos.java
package com.nexussoftware.bibliotech.domini.avisos.port;

import com.nexussoftware.bibliotech.domini.avisos.Avis;

/**
 * Port de sortida: el domini declara QUE necessita (notificar),
 * sense dir COM (correu, SMS, consola, cua de missatges).
 */
public interface NotificadorAvisos {
    void notificar(Avis avis);
}

El missatge com a objecte de domini, sense format de presentació:

// bibliotech-domini/…/domini/avisos/Avis.java
package com.nexussoftware.bibliotech.domini.avisos;

public record Avis(String destinatari,
                   TipusAvis tipus,
                   String nomEmpleat,
                   String titolMaterial,
                   LocalDate data) {

    public static Avis perVenciment(Prestec prestec) {
        return new Avis(
            prestec.correuDeLempleat(),            // metode del mateix Prestec: sense cadena de getters
            TipusAvis.VENCIMENT,
            prestec.nomDeLempleat(),
            prestec.titolDelMaterial(),
            prestec.getDataVenciment());
    }
}

El cas d'ús, a la capa d'aplicació:

// bibliotech-aplicacio/…/aplicacio/avisos/ServeiAvisos.java
package com.nexussoftware.bibliotech.aplicacio.avisos;

public class ServeiAvisos {

    private final NotificadorAvisos notificador;   // el PORT, no la implementacio
    private final RepositoriPrestecs prestecs;
    private final Clock rellotge;                  // 10-05: mai LocalDate.now() a pel

    public ServeiAvisos(NotificadorAvisos notificador,
                        RepositoriPrestecs prestecs,
                        Clock rellotge) {
        this.notificador = notificador;
        this.prestecs = prestecs;
        this.rellotge = rellotge;
    }

    public int avisarVencimentsProxims(int diesAntelacio) {
        LocalDate limit = LocalDate.now(rellotge).plusDays(diesAntelacio);
        List<Prestec> proxims = prestecs.vencutsA(limit);
        proxims.forEach(p -> notificador.notificar(Avis.perVenciment(p)));
        return proxims.size();
    }
}

L'adaptador, a infraestructura, que és l'únic que coneix SMTP, Spring i el format:

// bibliotech-infraestructura/…/infraestructura/avisos/NotificadorCorreu.java
package com.nexussoftware.bibliotech.infraestructura.avisos;

@Component
class NotificadorCorreu implements NotificadorAvisos {

    private static final Logger log = LoggerFactory.getLogger(NotificadorCorreu.class);

    private final JavaMailSender correu;
    private final PropietatsBiblioTech props;      // host, port i remitent venen d'aqui

    NotificadorCorreu(JavaMailSender correu, PropietatsBiblioTech props) {
        this.correu = correu;
        this.props = props;
    }

    @Override
    public void notificar(Avis avis) {
        var missatge = new SimpleMailMessage();
        missatge.setFrom(props.avisos().remitent());
        missatge.setTo(avis.destinatari());
        missatge.setSubject("BiblioTech: avis de venciment");
        missatge.setText("""
                Hola %s,

                El prestec de "%s" venc el %s.

                -- BiblioTech, Nexus Software
                """.formatted(avis.nomEmpleat(), avis.titolMaterial(), avis.data()));
        try {
            correu.send(missatge);
        } catch (MailException e) {
            // Frontera d'errors (06-07): traduir a l'excepcio del domini
            log.warn("No s'ha pogut enviar l'avis a {}", avis.destinatari(), e);
            throw new NotificacioFallidaException(avis.destinatari(), e);
        }
    }
}

I el cablatge, també a infraestructura:

// bibliotech-infraestructura/…/infraestructura/config/ConfiguracioAvisos.java
@Configuration
class ConfiguracioAvisos {

    @Bean
    ServeiAvisos serveiAvisos(NotificadorAvisos notificador,
                              RepositoriPrestecs prestecs,
                              Clock rellotge) {
        return new ServeiAvisos(notificador, prestecs, rellotge);
    }
}

Guany: ServeiAvisos es prova amb un NotificadorAvisos en memòria que desa els avisos en una llista. Ni correu, ni Spring, ni base de dades, ni xarxa. Mil·lisegons.

Solució 2

Disseny de la funcionalitat «informe mensual».

Repartiment per mòduls:

Peça Mòdul Paquet Rol
InformeMensual (record) domini domini.informes Objecte de domini: les dades de l'informe
LiniaMaterial, LiniaEmpleat domini domini.informes Objectes de valor
GeneradorInforme (interfície) domini domini.informes.port Port de sortida: convertir dades en bytes
MagatzemInformes (interfície) domini domini.informes.port Port de sortida: desar
RepositoriEstadistiques (interfície) domini domini.informes.port Port de sortida: consultar agregats
EmetreInformeMensual (interfície) aplicació aplicacio.informes Port d'entrada
ServeiInformeMensual aplicació aplicacio.informes Cas d'ús: orquestra els quatre ports
GeneradorInformePdf infraestructura infraestructura.informes Adaptador amb openpdf
MagatzemInformesFitxer infraestructura infraestructura.informes Adaptador NIO.2 (mòdul 7)
RepositoriEstadistiquesJpa infraestructura infraestructura.persistencia Adaptador amb JPQL d'agregació
PlanificadorInformes infraestructura infraestructura.planificacio Adaptador d'entrada: @Scheduled
OrdreInforme consola consola.ordres Adaptador d'entrada: Picocli
InformeController web web.informes Adaptador d'entrada: REST

Les interfícies clau:

// DOMINI: les dades de l'informe. Sense PDF, sense fitxers, sense dates de servidor.
package com.nexussoftware.bibliotech.domini.informes;

public record InformeMensual(YearMonth periode,
                             List<LiniaMaterial> mesPrestats,
                             List<LiniaEmpleat> mesMultats,
                             double percentatgeDevolucionsAmbRetard) {

    public String nomSuggerit() {
        return "bibliotech-%s.pdf".formatted(periode);   // yyyy-MM
    }
}

// PORT: convertir l'informe en bytes. El domini no sap que existeix el PDF.
package com.nexussoftware.bibliotech.domini.informes.port;

public interface GeneradorInforme {
    byte[] generar(InformeMensual informe);
    String tipusMime();    // "application/pdf", "text/csv"...
}

// PORT: on es desa. El domini no sap si es disc, S3 o base de dades.
public interface MagatzemInformes {
    URI guardar(String nom, byte[] contingut, String tipusMime);
}

// PORT: d'on surten els agregats. El domini no sap que es JPQL.
public interface RepositoriEstadistiques {
    List<LiniaMaterial> materialsMesPrestats(YearMonth periode, int limit);
    List<LiniaEmpleat> empleatsMesMultats(YearMonth periode, int limit);
    double percentatgeAmbRetard(YearMonth periode);
}

El cas d'ús, que és l'únic lloc on es reuneix tot:

// APLICACIO
package com.nexussoftware.bibliotech.aplicacio.informes;

public interface EmetreInformeMensual {           // port d'ENTRADA
    ResultatInforme emetre(YearMonth periode, boolean enviarPerCorreu);
}

public class ServeiInformeMensual implements EmetreInformeMensual {

    private final RepositoriEstadistiques estadistiques;
    private final GeneradorInforme generador;
    private final MagatzemInformes magatzem;
    private final NotificadorAvisos notificador;
    private final Clock rellotge;

    // constructor amb els cinc ports...

    @Override
    public ResultatInforme emetre(YearMonth periode, boolean enviarPerCorreu) {
        var informe = new InformeMensual(
                periode,
                estadistiques.materialsMesPrestats(periode, 10),
                estadistiques.empleatsMesMultats(periode, 10),
                estadistiques.percentatgeAmbRetard(periode));

        byte[] contingut = generador.generar(informe);
        URI ubicacio = magatzem.guardar(informe.nomSuggerit(), contingut, generador.tipusMime());

        if (enviarPerCorreu) {
            notificador.notificar(Avis.informeDisponible(periode, ubicacio));
        }
        return new ResultatInforme(periode, ubicacio, contingut.length);
    }
}

I els tres adaptadors d'entrada, que són tres maneres de cridar exactament el mateix cas d'ús:

// infraestructura: automatic el dia 1 a les 06:00
@Component
class PlanificadorInformes {
    private final EmetreInformeMensual cas;
    @Scheduled(cron = "0 0 6 1 * *")
    void mensual() { cas.emetre(YearMonth.now().minusMonths(1), true); }
}

// consola (12-03)
@Command(name = "informe", description = "Genera l'informe mensual d'us")
class OrdreInforme implements Callable<Integer> { … }

// web (12-04)
@PostMapping("/api/informes/{periode}")
ResponseEntity<InformeResponse> generar(@PathVariable YearMonth periode) { … }

El que importa de l'exercici: canviar el PDF per CSV és escriure un GeneradorInformeCsv; canviar el disc per S3 és escriure un MagatzemInformesS3. El cas d'ús i el domini no es toquen. Això és el que compra l'arquitectura hexagonal, i per això val la pena en una funcionalitat com aquesta i no en un CRUD.

Solució 3

Problemes trobats (nou):

# Problema Gravetat Per què
1 Contrasenya Nexus2026! al repositori Crítica Secret a Git; queda a l'historial per sempre
2 api-key: sk-live-9f3a... al repositori Crítica Clau de producció exposada
3 URL de la base de dades de producció al fitxer base Crítica Qualsevol que arrenqui en local escriu a producció
4 Usuari admin Alta Viola el mínim privilegi (12-07): l'app necessita CRUD, no DDL
5 ddl-auto: update Alta Modifica l'esquema de producció de manera no versionada ni reproduïble
6 include-stacktrace: always Alta Filtra estructura interna, versions i camins a qualsevol client
7 logging.level.root: DEBUG Mitjana Volum enorme, cost, i risc de registrar dades personals
8 show-sql: true Mitjana Duplicat del logging, sense paràmetres lligats, sorollós en producció
9 Tot al fitxer base, sense perfils Mitjana No hi ha separació entre entorns

Versió corregida. Fitxer base, application.yml — només el que és comú i cap secret:

spring:
  application:
    name: bibliotech
  jpa:
    open-in-view: false
    hibernate:
      ddl-auto: validate        # l'esquema el governa Flyway; validate detecta descuadres
  flyway:
    enabled: true

server:
  port: ${SERVER_PORT:8080}     # amb valor per defecte: no es un secret
  shutdown: graceful
  error:
    include-stacktrace: never
    include-message: never

bibliotech:
  metadades:
    url: https://api.metadades.exemple/v1
    temps-espera: 3s
    # api-key NO es aqui: arriba per variable d'entorn

logging:
  level:
    root: INFO
    com.nexussoftware.bibliotech: INFO

application-dev.yml — local, exhaurible, verbós:

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/bibliotech
    username: bibliotech
    password: bibliotech        # acceptable: base local en un contenidor efimer
  jpa:
    show-sql: true

server:
  error:
    include-stacktrace: on_param   # ?trace=true, comode en depurar

bibliotech:
  metadades:
    api-key: ${METADADES_API_KEY:clau-de-desenvolupament}

logging:
  level:
    com.nexussoftware.bibliotech: DEBUG
    org.hibernate.SQL: DEBUG
    org.hibernate.orm.jdbc.bind: TRACE   # veure els parametres lligats: nomes a dev

application-prod.yml — sense ni un sol valor per defecte en el que és sensible:

spring:
  datasource:
    url: ${BIBLIOTECH_DB_URL}
    username: ${BIBLIOTECH_DB_USER}         # usuari amb permisos minims, NO admin
    password: ${BIBLIOTECH_DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
  jpa:
    show-sql: false

bibliotech:
  metadades:
    api-key: ${METADADES_API_KEY}           # sense defecte: si falta, no arrenca

logging:
  level:
    root: WARN
    com.nexussoftware.bibliotech: INFO

Accions addicionals, i aquest és el punt de l'exercici: corregir el fitxer no n'hi ha prou. La contrasenya i la clau d'API ja són a l'historial de Git. El procediment correcte és, en aquest ordre:

  1. Rotar ja la contrasenya de la base de dades i la clau d'API. Estan compromeses.
  2. Crear un usuari de base de dades amb permisos mínims (SELECT, INSERT, UPDATE, DELETE sobre l'esquema de l'aplicació; mai DROP ni CREATE).
  3. Afegir gitleaks com a enganxall de pre-commit i com a pas de CI.
  4. Netejar l'historial (git filter-repo) només si el repositori és privat i controlat, sabent que reescriu tots els hashes i obliga que tot l'equip torni a clonar.
  5. Escriure un ADR sobre la gestió de secrets, perquè la decisió quedi documentada.

Es reprèn a 12-07.

Conclusió

BiblioTech ha deixat de ser un munt de classes excel·lents per convertir-se en un projecte.

Té una arquitectura explícita: capes amb la regla de dependència aplicada seriosament i ports on hi ha tecnologia externa. Saps distingir l'arquitectura per capes de l'hexagonal, en coneixes els avantatges i els costos reals, i —el més important— tens el criteri per no aplicar hexagonal completa a un CRUD ni capes laxes a un sistema amb regles de negoci riques. I has interioritzat la idea que ho sosté tot: la direcció de la dependència i la direcció del flux són coses diferents, i per això el domini pot definir la interfície que la infraestructura implementa.

Té una organització de paquets amb criteri: per funcionalitat al primer nivell, per capa a dins, amb la raó decisiva que només així pots amagar CalculadoraMultes darrere de la visibilitat de paquet. Els paquets per capa es degraden perquè obliguen a fer públic tot.

cinc mòduls Maven el graf de dependències dels quals converteix l'arquitectura en una cosa que el compilador verifica. Que bibliotech-domini no tingui dependències de producció no és un adorn: és la raó per la qual ningú no podrà ficar una anotació de Spring en una regla de negoci, ni avui ni d'aquí a dos anys amb pressa. I si el multimòdul no és viable, tens ArchUnit com a xarxa de seguretat.

DTO: entitats cap endins, record cap enfora, amb les sis raons concretes per les quals exposar una entitat JPA en una API acaba malament, i amb l'asimetria dels DTO d'entrada com a defensa estructural davant la manipulació de camps.

configuració per entorn amb perfils, la taula de precedència de fonts de Spring Boot, la relaxació de noms que fa possible configurar-ho tot per variables d'entorn, i la regla que en producció el que és sensible no porta valor per defecte: si falta, l'aplicació no arrenca. I té l'advertència que evita més incidents: els secrets, fora del repositori, sempre.

I té la higiene que separa un projecte professional d'un d'amateur: un .gitignore que cobreix el que importa, branques curtes, commits convencionals que permeten generar el registre de canvis, .editorconfig i Spotless per no discutir mai més sobre claus, ADR que responen al «per què està això així?» d'aquí a dos anys, un compose.yaml que aixeca les dependències amb una ordre, i un README que compleix l'únic criteri que importa: que algú nou arrenqui el projecte en deu minuts.

Queda un deute que aquest mòdul va obrir i no va tancar: els patrons. En reestructurar han tornat a aparèixer, ara gairebé tots alhora. RepositoriPrestecs amb la seva implementació JPA és el patró Repositori i també Adaptador. PassarelaMetadades és un Port. ReglaTarifa és Estratègia. FitxaPrestec.desDe(...) és un Mètode de fàbrica. La injecció per constructor és Inversió de dependències. Portes onze mòduls trobant-te aquestes estructures, anomenant-les de passada i ajornant-les.

S'ha acabat l'ajornament. La lliçó següent les formalitza totes: quin problema resol cada patró, com s'implementa a BiblioTech, i —tan important com l'anterior— quan no cal fer-lo servir.

Curs de Programació en Java

Mòdul 1: Introducció a Java

Mòdul 2: Flux de control

Mòdul 3: Programació orientada a objectes

Mòdul 4: Programació orientada a objectes avançada

Mòdul 5: Estructures de dades i col·leccions

Mòdul 6: Gestió d'excepcions

Mòdul 7: Entrada/sortida de fitxers

Mòdul 8: Multifil i concurrència

Mòdul 9: Xarxes

Mòdul 10: Temes avançats

Mòdul 11: Frameworks i llibreries de Java

Mòdul 12: Construcció d'aplicacions del món real

© Copyright 2026. Tots els drets reservats