Un backend che nasce come «controller, servizio, repository» funziona finché le regole di business sono tre righe. Quando diventano trenta, scopri che la regola «un socio non può avere più di tre prestiti» vive in un metodo che per essere provato ha bisogno del database, del contesto Spring e di una richiesta HTTP. La Clean Architecture risolve questo problema con una sola regola, che Robert C. Martin chiama Dependency Rule: le dipendenze del codice sorgente possono puntare solo verso l'interno, e niente in un cerchio interno può sapere qualcosa di un cerchio esterno. Questo capitolo costruisce una piccola API di prestiti bibliotecari in quattro strati, la fa girare nei test e fa controllare la regola da un test, non da una buona intenzione. Viene dopo il capitolo su Angular, perché è il lato che serve quei dati.
Il progetto di prova è mio, Maven, con Spring Boot 4.1.2 (l'ultima versione stabile nel repository Maven Central il 13 novembre 2026: la più recente in elenco è la 4.2.0-RC2, una release candidate), Java 21, Hibernate 7.4.13 e H2 2.4.240 in memoria al posto di un database vero. La documentazione dice che Spring Boot 4.1.2 richiede almeno Java 17 e Spring Framework 7.0.10. Non c'è nessuna porta aperta: le richieste passano da MockMvc, che invoca il DispatcherServlet dentro il processo del test.
Quattro cartelle, una direzione
Il progetto ha quattro pacchetti: domain per le regole, application per i casi d'uso, infrastructure per tutto ciò che parla con il database o con Spring, presentation per i controller. Le frecce vanno da fuori a dentro: la presentazione usa i casi d'uso, i casi d'uso usano il dominio, e l'infrastruttura implementa le interfacce che l'applicazione dichiara. Il dominio non importa niente. Ti serve il file pom.xml solo per vedere da dove arrivano le librerie, ed ecco le dipendenze usate (il resto è la configurazione standard del parent di Spring Boot).
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency>
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><scope>runtime</scope></dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webmvc-test</artifactId><scope>test</scope></dependency>
<dependency><groupId>com.tngtech.archunit</groupId><artifactId>archunit-junit5</artifactId><version>1.5.1</version><scope>test</scope></dependency>
I tre starter indicano che cosa fa l'applicazione: spring-boot-starter-webmvc per le API, spring-boot-starter-validation per i vincoli sul corpo delle richieste, spring-boot-starter-data-jpa per la persistenza. Il database h2 con scope runtime sta solo qui perché la prova non richiede un servizio esterno; in produzione al suo posto metteresti il driver del tuo database. Lo starter spring-boot-starter-webmvc-test porta MockMvc per i test, e la libreria ArchUnit, anch'essa con scope test, controlla le dipendenze fra i pacchetti.
Il dominio: una regola che si prova senza Spring
Il dominio contiene un prestito e la regola che ne limita il numero. Il record Loan ha un metodo statico open che riceve il numero di prestiti già attivi e rifiuta l'apertura del quarto con un'eccezione del dominio. Nessuna annotazione, nessun import da org.springframework o da jakarta: è Java e basta. Ho scelto di passare today come parametro invece di leggere l'ora dentro il metodo, perché così il test decide la data e il risultato non cambia da un giorno all'altro.
// domain/Loan.java
import java.time.LocalDate;
/** Un prestito. Nessuna dipendenza da Spring, da JPA o dal web: solo Java. */
public record Loan(Long id, String memberId, String isbn, LocalDate borrowedOn, LocalDate dueOn) {
public static final int MAX_ACTIVE_LOANS = 3;
public static final int LOAN_DAYS = 28;
/** Regola di business: un socio non può avere più di tre prestiti aperti. */
public static Loan open(String memberId, String isbn, LocalDate today, int activeLoans) {
if (activeLoans >= MAX_ACTIVE_LOANS) {
throw new LoanLimitExceeded(memberId, MAX_ACTIVE_LOANS);
}
return new Loan(null, memberId, isbn, today, today.plusDays(LOAN_DAYS));
}
}
// domain/LoanLimitExceeded.java
public class LoanLimitExceeded extends RuntimeException {
public LoanLimitExceeded(String memberId, int limit) {
super("Il socio " + memberId + " ha già " + limit + " prestiti aperti");
}
}
// LoanTest.java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import it.esempio.biblioteca.domain.Loan;
import it.esempio.biblioteca.domain.LoanLimitExceeded;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
/** Nessun contesto Spring: parte in pochi millisecondi. */
class LoanTest {
@Test
void scadeDopoVentottoGiorni() {
Loan loan = Loan.open("m1", "9788820399990", LocalDate.of(2026, 11, 13), 0);
assertEquals(LocalDate.of(2026, 12, 11), loan.dueOn());
System.out.println("scadenza: " + loan.dueOn());
}
@Test
void ilQuartoPrestitoEUnaViolazioneDiRegola() {
LoanLimitExceeded e = assertThrows(LoanLimitExceeded.class,
() -> Loan.open("m1", "9788820399990", LocalDate.of(2026, 11, 13), 3));
System.out.println("regola: " + e.getMessage());
}
}
// Output:
// regola: Il socio m1 ha già 3 prestiti aperti
// scadenza: 2026-12-11
Il test LoanTest non avvia nessun contesto: l'esecuzione ha impiegato, nelle mie esecuzioni, circa 0,006-0,008 secondi (macchina con 12 core, tre esecuzioni, numero riportato da Maven per la classe). Le due righe in uscita provengono dalle stampe del test: la scadenza è 28 giorni dopo il 13 novembre 2026, e al quarto prestito la regola alza LoanLimitExceeded. Una regola che si prova in pochi millesimi di secondo si prova di continuo; una che richiede un contesto da quattro secondi, come vedrai fra poco, si prova meno spesso di quanto dovrebbe.
L'applicazione: il caso d'uso e la porta
Il caso d'uso BorrowBook coordina: chiede quanti prestiti attivi ha il socio, chiede al dominio di aprirne uno, lo salva. Per parlare con la persistenza non usa JPA: dichiara un'interfaccia, LoanRepository, con i soli due metodi che gli servono. Questa interfaccia è la porta; chi la implementa sta più in fuori e dipende da lei, non viceversa. L'orologio arriva dal costruttore come Clock: in produzione è l'orologio di sistema, nei test uno fisso.
// application/LoanRepository.java
import it.esempio.biblioteca.domain.Loan;
/** Porta: l'applicazione dice che cosa le serve, l'infrastruttura decide come. */
public interface LoanRepository {
int countByMember(String memberId);
Loan save(Loan loan);
}
// application/BorrowBook.java
import it.esempio.biblioteca.domain.Loan;
import java.time.Clock;
import java.time.LocalDate;
/** Caso d'uso. Nessuna annotazione di Spring: si costruisce con il costruttore. */
public class BorrowBook {
public record Request(String memberId, String isbn) {}
public record Response(long id, String memberId, String isbn, LocalDate dueOn) {}
private final LoanRepository loans;
private final Clock clock;
public BorrowBook(LoanRepository loans, Clock clock) {
this.loans = loans;
this.clock = clock;
}
public Response execute(Request request) {
int active = loans.countByMember(request.memberId());
Loan opened = Loan.open(request.memberId(), request.isbn(), LocalDate.now(clock), active);
Loan saved = loans.save(opened);
return new Response(saved.id(), saved.memberId(), saved.isbn(), saved.dueOn());
}
}
La classe BorrowBook non ha annotazioni Spring e si costruisce con il suo costruttore: new BorrowBook(repository, clock). Il prezzo è che qualcuno deve dire a Spring come crearla, e lo fa un file dell'infrastruttura, come vedrai. Il vantaggio è che la regola dipendenze «l'applicazione non importa Spring» diventa verificabile. Il compromesso è reale: molti progetti scelgono di mettere @Service sul caso d'uso per risparmiare quel file, e perdono un confine in cambio di meno codice. Se il tuo team è piccolo e il dominio semplice, può essere la scelta giusta; se prevedi di cambiare framework o di riusare i casi d'uso da un job, tieni il confine.
L'infrastruttura: JPA dietro la porta
Qui vive tutto ciò che conosce il database. LoanEntity è l'entità JPA, separata dal record del dominio: costa una conversione in più, in cambio il dominio non è costretto ad avere un costruttore vuoto e campi modificabili solo perché JPA lo chiede. JpaLoanRepository implementa la porta appoggiandosi a un'interfaccia Spring Data, e BeanConfig registra BorrowBook e l'orologio. Le classi sono visibili solo dentro il pacchetto, per impedire a un controller di usarle direttamente.
// infrastructure/LoanEntity.java
import it.esempio.biblioteca.domain.Loan;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.time.LocalDate;
@Entity
@Table(name = "loan")
class LoanEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String memberId;
private String isbn;
private LocalDate borrowedOn;
private LocalDate dueOn;
protected LoanEntity() {} // richiesto da JPA
static LoanEntity from(Loan loan) {
LoanEntity e = new LoanEntity();
e.id = loan.id();
e.memberId = loan.memberId();
e.isbn = loan.isbn();
e.borrowedOn = loan.borrowedOn();
e.dueOn = loan.dueOn();
return e;
}
Loan toDomain() {
return new Loan(id, memberId, isbn, borrowedOn, dueOn);
}
}
// infrastructure/LoanJpaRepository.java
import org.springframework.data.jpa.repository.JpaRepository;
interface LoanJpaRepository extends JpaRepository<LoanEntity, Long> {
int countByMemberId(String memberId);
}
// infrastructure/JpaLoanRepository.java
import it.esempio.biblioteca.application.LoanRepository;
import it.esempio.biblioteca.domain.Loan;
import org.springframework.stereotype.Repository;
@Repository
class JpaLoanRepository implements LoanRepository {
private final LoanJpaRepository jpa;
JpaLoanRepository(LoanJpaRepository jpa) {
this.jpa = jpa;
}
@Override
public int countByMember(String memberId) {
return jpa.countByMemberId(memberId);
}
@Override
public Loan save(Loan loan) {
return jpa.save(LoanEntity.from(loan)).toDomain();
}
}
// infrastructure/BeanConfig.java
import it.esempio.biblioteca.application.BorrowBook;
import it.esempio.biblioteca.application.LoanRepository;
import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class BeanConfig {
@Bean
Clock clock() {
return Clock.systemDefaultZone();
}
@Bean
BorrowBook borrowBook(LoanRepository loans, Clock clock) {
return new BorrowBook(loans, clock);
}
}
Il metodo countByMemberId è una derived query: Spring Data ricava la query dal nome del metodo. Il file di configurazione della prova imposta tre proprietà: spring.jpa.open-in-view=false, spring.jpa.hibernate.ddl-auto=create-drop e l'URL dell'H2 in memoria. La documentazione di Spring Boot riporta che open-in-view vale true se non lo scrivi, e in quel caso un EntityManager resta legato al thread per tutta l'elaborazione della richiesta: spento, ogni accesso ai dati deve avvenire nel livello che lo dichiara. Per ddl-auto la stessa pagina dice che il valore è create-drop con un database incorporato e nessun gestore dello schema, altrimenti none: l'ho scritto per chiarezza, ma in produzione lo schema lo cambi con le migrazioni, non con Hibernate.
La presentazione: validare, rispondere, spiegare gli errori
Il controller traduce HTTP nel linguaggio del caso d'uso e viceversa, e basta. Il corpo della richiesta è un record con due vincoli di Bean Validation: @NotBlank sul socio e @Pattern sull'ISBN a 13 cifre. Il controller risponde 201 con l'intestazione Location, che indica dove si trova la risorsa appena creata. Gli errori sono affidati a una classe @RestControllerAdvice che estende ResponseEntityExceptionHandler.
// presentation/BorrowRequest.java
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
public record BorrowRequest(
@NotBlank(message = "obbligatorio") String memberId,
@Pattern(regexp = "\\d{13}", message = "l'ISBN ha 13 cifre") String isbn) {}
// presentation/LoanController.java
import it.esempio.biblioteca.application.BorrowBook;
import jakarta.validation.Valid;
import java.net.URI;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/loans")
class LoanController {
private final BorrowBook borrowBook;
LoanController(BorrowBook borrowBook) {
this.borrowBook = borrowBook;
}
@PostMapping
ResponseEntity<BorrowBook.Response> borrow(@Valid @RequestBody BorrowRequest body) {
BorrowBook.Response done = borrowBook.execute(new BorrowBook.Request(body.memberId(), body.isbn()));
return ResponseEntity.created(URI.create("/api/v1/loans/" + done.id())).body(done);
}
}
// presentation/ApiExceptionHandler.java
import it.esempio.biblioteca.domain.LoanLimitExceeded;
import java.util.Map;
import java.util.TreeMap;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@RestControllerAdvice
class ApiExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(LoanLimitExceeded.class)
ProblemDetail limit(LoanLimitExceeded e) {
ProblemDetail p = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, e.getMessage());
p.setTitle("Limite prestiti raggiunto");
return p;
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
Map<String, String> errors = new TreeMap<>();
for (FieldError f : e.getBindingResult().getFieldErrors()) {
errors.putIfAbsent(f.getField(), String.valueOf(f.getDefaultMessage()));
}
ProblemDetail p = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "La richiesta non è valida");
p.setProperty("errors", errors);
return ResponseEntity.badRequest().body(p);
}
}
La documentazione di Spring spiega che ProblemDetail rappresenta un errore secondo RFC 9457 e che ResponseEntityExceptionHandler è la classe base per gestire le eccezioni di Spring MVC con quel formato. L'RFC 9457, del luglio 2023, sostituisce la RFC 7807 e definisce il tipo di contenuto application/problem+json. Nel gestore ho scritto due scelte mie. La violazione della regola di business diventa un 409 con titolo comprensibile. Per la validazione ho ridefinito handleMethodArgumentNotValid per aggiungere una proprietà errors con un messaggio per campo, perché il comportamento predefinito mi ha restituito solo «Invalid request content.» senza dire quale campo fosse sbagliato (l'ho visto nella prima esecuzione).
Il percorso completo, senza aprire una porta
Il test seguente avvia il contesto Spring completo con H2 in memoria e usa MockMvc per spedire quattro prestiti validi e una richiesta sbagliata. Un orologio fisso, definito in una @TestConfiguration con @Primary, rende la data di scadenza uguale in ogni esecuzione. Senza di lui, la prima versione del test stampava una data che sarebbe cambiata il giorno dopo.
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
@SpringBootTest
@AutoConfigureMockMvc
class LoanApiTest {
@TestConfiguration
static class FixedClock {
@Bean
@Primary
Clock fixedClock() {
return Clock.fixed(Instant.parse("2026-11-13T09:00:00Z"), ZoneOffset.UTC);
}
}
@Autowired
MockMvc mvc;
private void send(String json) throws Exception {
MvcResult r = mvc.perform(post("/api/v1/loans").contentType(MediaType.APPLICATION_JSON).content(json)).andReturn();
var res = r.getResponse();
System.out.println("HTTP " + res.getStatus() + " " + res.getContentType()
+ (res.getHeader("Location") == null ? "" : " Location=" + res.getHeader("Location")));
System.out.println(" " + res.getContentAsString());
}
@Test
void tuttoIlPercorso() throws Exception {
String ok = "{\"memberId\":\"m7\",\"isbn\":\"9788820399990\"}";
for (int i = 0; i < 4; i++) send(ok);
send("{\"memberId\":\"\",\"isbn\":\"123\"}");
}
}
// Output:
// HTTP 201 application/json Location=/api/v1/loans/1
// {"id":1,"memberId":"m7","isbn":"9788820399990","dueOn":"2026-12-11"}
// HTTP 201 application/json Location=/api/v1/loans/2
// {"id":2,"memberId":"m7","isbn":"9788820399990","dueOn":"2026-12-11"}
// HTTP 201 application/json Location=/api/v1/loans/3
// {"id":3,"memberId":"m7","isbn":"9788820399990","dueOn":"2026-12-11"}
// HTTP 409 application/problem+json
// {"detail":"Il socio m7 ha già 3 prestiti aperti","instance":"/api/v1/loans","status":409,"title":"Limite prestiti raggiunto"}
// HTTP 400 application/problem+json
// {"detail":"La richiesta non è valida","instance":"/api/v1/loans","status":400,"title":"Bad Request","errors":{"isbn":"l'ISBN ha 13 cifre","memberId":"obbligatorio"}}
L'uscita contiene tre risposte diverse, e ciascuna ha un significato per chi consuma l'API:
- 201 con
Location: il prestito è stato creato; il corpo ha l'identificativo e la scadenza. - 409 con
application/problem+json: la richiesta era valida ma la regola di business la rifiuta, e il client può mostrare il messaggio al socio. - 400 con
application/problem+jsone la mappaerrors: la richiesta era sbagliata, e il client sa quali campi correggere.
La distinzione fra 409 e 400 è la ragione per cui conviene tenere separate la validazione e la regola. Il BorrowRequest controlla la forma («13 cifre»), il dominio controlla il senso («il quarto prestito no»). Se mettessi il limite dei tre prestiti nel validatore, il client non potrebbe distinguere un errore di battitura da un divieto, e ogni altro canale di ingresso, per esempio un job notturno, dovrebbe ripetere la regola. Questo test costa circa 4 secondi (4,0-4,2 s nelle mie esecuzioni), quasi tutti per avviare il contesto.
Far rispettare le frecce con un test
Una regola di dipendenza scritta in un documento si dimentica; scritta in un test, fa fallire la build. ArchUnit analizza le classi compilate e permette di scrivere regole come «nessuna classe del dominio dipende da Spring». Il listato ne contiene due, una per il dominio e una per l'applicazione. Usa pacchetti con la notazione ..domain.. (qualsiasi pacchetto che contenga domain).
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.Test;
class LayerRulesTest {
private final JavaClasses classes = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importPackages("it.esempio.biblioteca");
@Test
void ilDominioNonConoscePiuNiente() {
noClasses().that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework..", "jakarta..", "..application..", "..infrastructure..", "..presentation..")
.check(classes);
}
@Test
void l_applicazioneNonConosceInfrastrutturaEWeb() {
noClasses().that().resideInAPackage("..application..")
.should().dependOnClassesThat().resideInAnyPackage(
"org.springframework..", "jakarta..", "..infrastructure..", "..presentation..")
.check(classes);
}
}
Con il codice pulito il test passa: mvn -B test riporta cinque test e zero fallimenti, distribuiti in tre classi: LayerRulesTest (due regole), LoanTest (due test sul dominio) e LoanApiTest (il percorso HTTP). Per vedere la rete fare il suo lavoro ho aggiunto, per un solo run e poi tolto, una classe di dominio che porta un'annotazione di Spring, e ho rilanciato solo LayerRulesTest. La build è fallita con questo messaggio, senza avviare il resto dell'applicazione.
package it.esempio.biblioteca.domain;
import org.springframework.stereotype.Component;
@Component
public class BadRule {}
// Output di ArchUnit (righe principali):
// Architecture Violation [Priority: MEDIUM] - Rule 'no classes that reside in a package '..domain..' should depend on classes that reside in any package ['org.springframework..', 'jakarta..', '..application..', '..infrastructure..', '..presentation..']' was violated (1 times):
// Class <it.esempio.biblioteca.domain.BadRule> is annotated with <org.springframework.stereotype.Component> in (BadRule.java:0)
Il messaggio nomina la classe e l'annotazione che viola la regola: non devi cercarle. Quel che questo test non cattura è il modo in cui il codice è organizzato dentro un pacchetto, né se una regola di business sia finita nel controller invece che nel dominio: quello lo vede solo una revisione del codice. Non usare ArchUnit come sostituto di una revisione, ma come cancello per ciò che una macchina sa contare, cioè gli import.
Domande frequenti
Serve davvero separare l'entità JPA dal modello del dominio?
Dipende dal costo che sei disposto a pagare. Con due classi scrivi una conversione e il dominio resta libero da vincoli di JPA, come il costruttore vuoto; con una sola classe scrivi meno codice e il dominio importa jakarta.persistence. In questo progetto di prova ho separato le due, e il test ArchUnit lo rende verificabile.
Perché usare ProblemDetail invece di un oggetto di errore mio?
Perché è un formato standard, l'RFC 9457, con il tipo di contenuto application/problem+json, che i client possono già interpretare. Spring lo supporta con ProblemDetail e con ResponseEntityExceptionHandler, e permette di aggiungere campi propri, come ho fatto con errors.
Che cosa vale open-in-view se non lo scrivo?
Secondo la documentazione di Spring Boot vale true: un EntityManager resta legato al thread per tutta la richiesta. Nel progetto di prova l'ho messo a false, così i dati vanno letti dentro il livello che li gestisce.
Che cosa sai fare adesso
Sai dividere un backend Spring Boot in quattro strati e spiegare perché le frecce vanno verso il dominio. Sai scrivere una regola di business che si prova senza contesto in pochi millesimi di secondo, dichiarare una porta nell'applicazione e implementarla con JPA fuori. Sai restituire errori come application/problem+json distinguendo la richiesta malformata (400) dalla regola violata (409), e sai far fallire la build quando qualcuno importa Spring nel dominio. Il capitolo successivo mette a confronto due modi di esporre questi dati, GraphQL e REST; l'elenco della serie è nella pagina dedicata.
La serie si chiude con CI/CD multi-ambiente, che ne raccoglie l'elenco degli otto articoli.
Fonti
- Robert C. Martin, The Clean Architecture (2012) - la Dependency Rule («source code dependencies can only point inwards»), letta il 2026-11-13
- RFC 9457, Problem Details for HTTP APIs - formato dei dettagli del problema, tipo di contenuto
application/problem+json, sostituisce la RFC 7807, letta il 2026-11-13 - Spring Framework, Error Responses -
ProblemDetail,ErrorResponseeResponseEntityExceptionHandler, letta il 2026-11-13 - Spring Boot, System Requirements - Spring Boot 4.1.2 richiede almeno Java 17 e Spring Framework 7.0.10, letta il 2026-11-13
- Spring Boot, Common Application Properties - valori predefiniti di
spring.jpa.open-in-view(true) espring.jpa.hibernate.ddl-auto, pagina scaricata e letta il 2026-11-13 - ArchUnit User Guide - regole di dipendenza tra pacchetti con
noClasses(), pagina scaricata il 2026-11-13 - Maven Central, spring-boot-starter-parent - elenco delle versioni (4.1.2 ultima stabile, 4.2.0-RC2 candidata), interrogato il 2026-11-13
Commenti
Caricamento commenti...
Accedi per commentare