Architecture
Architecture hexagonale avec Spring Boot 4 : ce que cela change vraiment
Créer trois dossiers ne suffit pas à faire une architecture hexagonale. Voici la règle de dépendance, le code qui la respecte et les tests qu'elle rend enfin possibles avec Spring Boot 4 et Java 21.
Adopter une architecture hexagonale ne consiste pas à renommer des paquets. Le bénéfice apparaît uniquement si la règle de dépendance est respectée dans le code, vérifiable par le compilateur et par les tests. Voici comment nous procédons concrètement sur un projet Spring Boot 4, et ce que cela change au quotidien pour une équipe.
Pourquoi le découpage par couches ne suffit plus
Le découpage classique en contrôleurs, services et dépôts produit rapidement une dépendance généralisée : le service métier connaît les annotations de persistance, le format des requêtes HTTP et parfois le nom des files de messages. Tester une règle de facturation demande alors de démarrer un contexte Spring complet, une base de données et un courtier. Le coût se paie en lenteur de test et en rigidité dès qu'une technologie doit changer.
La conséquence pratique est connue : les tests unitaires deviennent des tests d'intégration déguisés, leur durée passe de quelques millisecondes à plusieurs secondes, et l'équipe finit par ne plus les exécuter avant de pousser du code.
Le principe : le domaine ne dépend de rien
L'architecture hexagonale inverse la direction des dépendances. Le domaine contient les règles métier et n'utilise que le langage Java standard. L'application orchestre les cas d'usage et définit des interfaces, appelées ports, dont elle a besoin. L'infrastructure fournit les implémentations : base de données, API HTTP, courtier de messages, fournisseur d'identité.
package com.example.billing.domain;
import java.math.BigDecimal;
import java.time.LocalDate;
public final class Invoice {
private final InvoiceId id;
private final BigDecimal amount;
private final LocalDate dueDate;
private InvoiceStatus status;
Invoice(InvoiceId id, BigDecimal amount, LocalDate dueDate) {
this.id = id;
this.amount = amount;
this.dueDate = dueDate;
this.status = InvoiceStatus.ISSUED;
}
public void registerPayment(LocalDate paymentDate) {
if (status == InvoiceStatus.PAID) {
throw new IllegalStateException("Invoice " + id.value() + " is already paid");
}
this.status = paymentDate.isAfter(dueDate) ? InvoiceStatus.PAID_LATE : InvoiceStatus.PAID;
}
}
Ce code ne contient aucune annotation. Il se teste en quelques millisecondes, sans contexte Spring et sans base de données. Le cas d'usage, lui, dépend d'un port qu'il définit lui-même : aucun import issu de l'infrastructure n'apparaît dans le paquet application.
package com.example.billing.application;
import java.time.Clock;
import java.time.LocalDate;
public class SettleInvoiceUseCase {
private final InvoiceRepository repository;
private final Clock clock;
public SettleInvoiceUseCase(InvoiceRepository repository, Clock clock) {
this.repository = repository;
this.clock = clock;
}
public void settle(InvoiceId id) {
Invoice invoice = repository.findById(id)
.orElseThrow(() -> new InvoiceNotFoundException(id));
invoice.registerPayment(LocalDate.now(clock));
repository.save(invoice);
}
}
L'assemblage se fait dans une classe de configuration, ce qui garde l'application libre de toute annotation de framework et rend la dépendance explicite.
Les adaptateurs : un par technologie
Chaque technologie dispose de son adaptateur. Un adaptateur entrant traduit une requête HTTP ou un message en appel de cas d'usage. Un adaptateur sortant implémente un port en s'appuyant sur JPA, un client HTTP ou un courtier. Aucun adaptateur n'appelle un autre adaptateur : ils passent tous par l'application.
Une règle de nommage qui aide en revue de code
Nommer les paquets adapter.in.web, adapter.out.persistence et application.port.out rend les violations visibles immédiatement en revue de code. Une dépendance interdite se repère à l'oeil, sans outil supplémentaire.
- Le domaine ne contient que des objets métier et des exceptions métier.
- L'application contient les cas d'usage et les ports d'entrée et de sortie.
- Les adaptateurs contiennent toute la technologie : JPA, HTTP, Kafka, OAuth.
- La configuration assemble les objets ; elle est le seul endroit qui connaît Spring.
- Les schémas d'API et les entités JPA ne sont jamais exposés au domaine.
Tests : ce que l'architecture rend possible
Le gain principal est la possibilité de choisir le niveau de test adapté, au lieu de subir un test lent pour vérifier une règle simple.
| Niveau | Ce qui est vérifié | Durée indicative |
|---|---|---|
| Domaine | Règles métier, cas limites, invariants | Quelques millisecondes |
| Application | Orchestration avec des ports simulés | Quelques millisecondes |
| Adaptateur | Requêtes SQL, sérialisation, gestion des erreurs | Quelques secondes avec Testcontainers |
| Bout en bout | Parcours complet sur une version déployée | Quelques minutes |
Une architecture hexagonale ne se voit pas sur un schéma : elle se voit le jour où vous remplacez un adaptateur sans modifier une seule ligne de règle métier.
Trois erreurs fréquentes
La première consiste à faire dépendre le domaine d'une bibliothèque utilitaire externe, puis d'en ajouter une deuxième, jusqu'à ce que le domaine dépende indirectement du framework. La deuxième est de placer les validations métier dans les contraintes d'entité JPA : elles deviennent alors impossibles à tester sans base et difficiles à retrouver. La troisième est de créer des ports par technologie plutôt que par besoin, ce qui produit des interfaces calquées sur les outils et non sur le métier.
Ce qu'il faut retenir
L'architecture hexagonale est avant tout une règle de dépendance, pas une structure de dossiers. Elle se juge à un indicateur simple : la part de votre logique métier testable sans démarrer d'infrastructure. Sur un projet Spring Boot 4, cette part peut dépasser quatre-vingts pour cent, ce qui change réellement le rythme de développement et la confiance dans les livraisons.
Articles associes
JWT et rotation des jetons de rafraîchissement : la partie que l'on oublie souvent
Un jeton d'accès court est une bonne pratique. Sans rotation ni détection de réutilisation côté rafraîchissement, un jeton volé reste exploitable silencieusement pendant des semaines.