Index
ArchitectureCraftArchUnitHexagonal Architecture

ArchUnit & Architecture Hexagonale : Maintenir une Architecture Propre

Comment ArchUnit aide à maintenir une architecture hexagonale propre et évolutive

Thomas Maurier — Fullstack Developer · 1 décembre 2025 · 15 min

ArchUnit & Architecture Hexagonale : Maintenir une Architecture Propre

Introduction : garder une architecture propre… même quand le réel s’en mêle

Au début d’un projet, tout est simple : l’architecture est claire, le domaine isolé, l’infrastructure rangée à sa place. Les schémas sont nets, et tout le monde s’accorde sur les intentions.

Puis la réalité du terrain arrive.

Les sprints s’enchaînent, les deadlines se rapprochent, on corrige vite, on ajoute une « petite exception » qui « ne fera pas de mal ». Une classe métier finit par appeler un repository, un contrôleur se charge un peu trop, et les frontières qu’on pensait solides commencent à s’effriter. La dette technique ne s’installe pas d’un coup : elle s’accumule décision après décision, sans qu’on la remarque vraiment.

C’est là qu’ArchUnit change la donne. Cet outil transforme les principes architecturaux en règles concrètes, testées, visibles par toute l’équipe : on peut vérifier, à chaque commit, qu’une architecture reste fidèle à ce qu’elle était censée être.


Pourquoi tester son architecture

ArchUnit propose également une approche fonctionnelle fluide, où l’on peut combiner des prédicats pour exprimer des règles plus fines grâce à des opérateurs tels que and() et or(). Cela rend les tests plus expressifs et proches d’un langage naturel. Par exemple :

java
@ArchTest
static final ArchRule domain_should_only_depend_on_allowed_packages =
  noClasses()
      .that().resideInAPackage("..domain..")
      .should().dependOnClassesThat()
      .resideInAnyPackage("..infrastructure..")
      .orShould().dependOnClassesThat()
      .resideInAnyPackage("..ui..");

Ce type de combinaison illustre la souplesse du DSL ArchUnit : les règles se composent comme des fonctions, enchaînant conditions et exceptions avec une grande lisibilité.

On obtient ainsi un cadre d’expérimentation continue, où les règles peuvent évoluer au même rythme que l’architecture.

Les tests unitaires assurent la validité fonctionnelle du code ; les tests d’architecture, quant à eux, assurent la validité structurelle du système. Ces derniers permettent de vérifier que les frontières conceptuelles restent intactes et que les dépendances n’évoluent pas de manière incontrôlée.

Aucune architecture ne conserve sa pureté spontanément : sur un projet d’une certaine taille, des dépendances « parasites » finissent toujours par apparaître. ArchUnit offre un langage déclaratif pour formaliser des règles de dépendances et les exécuter comme des tests standards.

Exemple :

java·

ArchitectureTest.java

@AnalyzeClasses(packages = "com.mycompany.myapp")
class ArchitectureTest {

  @ArchTest
  static final ArchRule domain_should_not_depend_on_infrastructure =
      noClasses()
          .that().resideInAPackage("..domain..")
          .should().dependOnClassesThat()
          .resideInAnyPackage("..infrastructure..");

}

Cette règle simple illustre une garantie essentielle : le cœur métier ne dépend pas de la technique. Lorsqu’une dépendance violant ce principe est introduite, le test échoue, signalant une dérive avant qu’elle ne se généralise. Ces tests constituent ainsi un filet de sécurité conceptuel, permettant de prévenir plutôt que guérir.

ArchUnit n'analyse pas tes fichiers .java, mais ton bytecode .class. Autrement dit, il examine la version compilée de ton projet : indépendant de ton IDE, de ton style de code ou de tes commentaires, il voit ce que la JVM voit.


Architecture hexagonale : du concept à la vérification systématique

L’architecture hexagonale, ou modèle Ports and Adapters, sépare clairement la logique métier des détails techniques : les dépendances pointent toujours vers le domaine, jamais l’inverse.

Le domaine concentre la logique métier pure, indépendante de tout framework. Les ports définissent les points d’interaction abstraits entre le domaine et le reste du système. Les adaptateurs implémentent ces ports pour gérer les détails d’intégration : JPA, REST, files de messages, etc.

Cette structure améliore la testabilité et la maintenabilité. ArchUnit vérifie que le code respecte réellement cette hiérarchie — par exemple, qu’aucune dépendance ne circule du domaine vers l’infrastructure ou les frameworks externes, et que les flux de responsabilités restent orientés dans le bon sens à chaque build.


ArchUnit : du principe à la pratique quotidienne

Pour illustrer concrètement le rôle d’ArchUnit, considérons un cas fréquent : un développeur introduit, par inadvertance, une dépendance de l’infrastructure vers le domaine. Par exemple, un service métier commence à appeler directement un JpaRepository pour simplifier une requête. Sans garde-fou, cette erreur passe inaperçue.

Grâce à ArchUnit, la règle définie précédemment déclenche une alerte immédiate : le test échoue, indiquant qu’une classe du domaine dépend d’un élément d’infrastructure. La correction devient alors explicite : extraire l’appel dans un adaptateur et injecter le port approprié dans le service métier.

java
@ArchTest
static final ArchRule domain_should_not_call_repository_directly =
  noClasses()
      .that().resideInAPackage("..domain..")
      .should().callMethodWhere(targetIsInPackage("..repository.."));

Cette approche favorise la lisibilité du test : les règles ArchUnit s’écrivent comme des phrases en anglais courant, proches d’une logique de programmation fonctionnelle. On combine des prédicats (that, should, resideInAPackage) pour exprimer des intentions claires et human readable. Le résultat est un code de test presque auto-documenté, où chaque règle raconte une histoire métier : « le domaine ne parle pas directement à la base de données ».

ArchUnit s’intègre bien à la chaîne de développement continue : les tests d’architecture font partie du cycle de validation standard, au même titre que les tests unitaires et d’intégration, et deviennent avec le temps une forme de documentation vivante.

Quelques exemples de règles utiles et simples à mettre en place :

  • Interdire les dépendances du domaine vers les packages springframework.*.
  • S’assurer que les controllers résident uniquement dans ..web...
  • Empêcher les services applicatifs d’interagir directement avec la couche d’infrastructure.
java
// Empêcher les cycles (packages)
@ArchTest
static final ArchRule no_cycles =
slices().matching("com.myapp.(*)..").should().beFreeOfCycles();

// Les controllers restent dans ..web..
@ArchTest
static final ArchRule controllers_in_web =
classes().that().haveSimpleNameEndingWith("Controller")
.should().resideInAnyPackage("..web..");

// Les services applicatifs ne dépendent pas d'infrastructure
@ArchTest
static final ArchRule app_services_no_infra =
classes().that().resideInAnyPackage("..application..")
.should().onlyDependOnClassesThat()
.resideOutsideOfPackages("..infrastructure..");

// Le domaine n’importe pas Spring
@ArchTest
static final ArchRule domain_should_ignore_spring =
noClasses().that().resideInAnyPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage("org.springframework..");

Au-delà des règles simples, ArchUnit permet aussi d’exprimer des contraintes plus avancées : interdiction de dépendances circulaires, validation du nommage des packages, ou cohérence entre la structure des dossiers et celle des modules.


Intégration progressive dans un projet existant

Exemple de règle personnalisée : il est possible de vérifier que toutes les classes d’un package donné respectent une convention de nommage spécifique, par exemple qu’elles se terminent par Service. Cela permet de maintenir une cohérence de conception dans les couches applicatives :

java
@ArchTest
static final ArchRule services_should_be_named_properly =
  classes()
      .that().resideInAPackage("..application.service..")
      .should().haveSimpleNameEndingWith("Service");

Cette règle, simple mais efficace, garantit que la convention de nommage reste homogène, favorisant ainsi la lisibilité du code et la reconnaissance rapide des rôles des classes.

Introduire ArchUnit ne demande pas de refonte majeure : une démarche incrémentale suffit.

  1. Ajouter la dépendance dans le module de test :
xml·

pom.xml

<dependency>
  <groupId>com.tngtech.archunit</groupId>
  <artifactId>archunit</artifactId>
  <version>1.4.1</version>
  <scope>test</scope>
</dependency>
  1. Créer un dossier dédié, par exemple src/test/java/.../architecture, pour centraliser les règles.
  2. Écrire d’abord quelques règles essentielles (dépendances entre couches, nommage), puis les enrichir progressivement.
  3. Les exécuter en CI, pour garantir la cohérence architecturale à chaque merge.
  4. Partager les règles et les retours d’expérience avec le reste de l’équipe.

Une fois en place, la validation architecturale devient une habitude plutôt qu’une contrainte ponctuelle, et chaque nouveau développeur hérite d’un cadre de référence clair.


ArchUnit, le craft au quotidien

La lisibilité du DSL d’ArchUnit, déjà évoquée plus haut, est un vrai atout. Mais l’apport principal se situe ailleurs : les règles d’architecture deviennent explicites, écrites noir sur blanc et versionnées avec le code. Leur non-respect se détecte tout de suite, pas six mois plus tard en revue de code.

Chaque dépendance ajoutée est un choix. ArchUnit ne remplace ni la réflexion ni la conception, mais il garde une trace vérifiable de ces choix — et évite qu’un raccourci pris sous la pression d’une deadline se transforme, quelques mois plus tard, en migration coûteuse.

Combinés à une architecture hexagonale bien pensée, ces tests permettent à une structure de rester lisible et robuste sprint après sprint, pas uniquement le jour du kickoff.

Continuer la lecture