Back to all articles
ArchitectureCraftArchUnitHexagonal Architecture
15 min

ArchUnit & Architecture Hexagonale : Maintenir une Architecture Propre

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

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 : on définit une architecture claire, propre, bien découpée. Le domaine est isolé, l’infrastructure rangée, l’application structurée. Les schémas sont limpides, les intentions partagées, tout semble sous contrôle.

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 que l’on pensait gravées dans le marbre commencent à s’effriter. Lentement, presque invisiblement, la dette technique s’installe.

Ce n’est pas un manque de rigueur : c’est la vie normale d’un code qui évolue.

Et c’est justement là qu’ArchUnit change la donne. Cet outil transforme vos principes architecturaux en règles concrètes, testées, visibles par toute l’équipe. L’objectif n’est pas seulement de concevoir une architecture propre, mais de s’assurer qu’elle le reste, indépendamment du temps, des contraintes et des priorités du moment.


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 :

@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. L’expérience montre que les dépendances « parasites » apparaissent naturellement dans tout projet d’une certaine taille. ArchUnit offre un langage déclaratif pour formaliser des règles de dépendances et les exécuter comme des tests standards.

Exemple :

@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. Résultat : il est 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, offre une séparation claire entre la logique métier et les interactions techniques. Elle s’appuie sur une philosophie de dépendances orientée vers le domaine.

  • 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 les autres couches.
  • Les adaptateurs implémentent ces ports pour gérer les détails d’intégration (JPA, REST, files, etc.).

Cette structure favorise la testabilité, la maintenabilité et la compréhension du système. ArchUnit vient renforcer cette approche en vérifiant la conformité du code à cette hiérarchie. Par exemple, on peut s’assurer qu’aucune dépendance ne circule du domaine vers l’infrastructure ou les frameworks externes.

Au-delà de cela, cette discipline garantit que les flux de données et de responsabilités restent unidirectionnels. L’architecture hexagonale cesse alors d’être un simple modèle mental pour devenir un contrat vérifiable.


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.

@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 parfaitement à la chaîne de développement continue. Son rôle dépasse le simple contrôle statique : il devient un outil de documentation vivante et de prévention collective. Les tests ArchUnit font partie du cycle de validation standard, à la même hauteur que les tests unitaires et d’intégration.

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.
// 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 également d’exprimer des règles plus avancées : interdiction de dépendance circulaire, validation du nommage des packages, ou encore cohérence entre la structure physique (dossiers) et logique (modules). Ces règles offrent une capacité d’analyse continue de l’évolution structurelle du projet.


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 :

@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.

L’introduction d’ArchUnit ne nécessite pas de refonte majeure. Une démarche incrémentale suffit pour obtenir des résultats rapides :

  1. Ajout de la dépendance dans le module de test :
<dependency>
    <groupId>com.tngtech.archunit</groupId>
    <artifactId>archunit</artifactId>
    <version>1.4.1</version>
    <scope>test</scope>
</dependency>
  1. Création d’un dossier dédié : src/test/java/.../architecture pour centraliser les règles.
  2. Rédaction de règles essentielles (dépendances entre domaines, nommage, couches) puis enrichissement progressif.
  3. Exécution dans la CI pour garantir la cohérence architecturale à chaque merge ou release.
  4. Partage de modèles et bonnes pratiques au sein de l’équipe ou de la communauté interne.

Ce processus, une fois institutionnalisé, transforme la validation architecturale en habitude collective, non en contrainte ponctuelle. Chaque nouveau développeur bénéficie ainsi d’un cadre de référence clair.


ArchUnit, un levier pour le craft et la transmission

L’un des points forts d’ArchUnit réside dans la lisibilité de son DSL (Domain-Specific Language), conçu pour être human readable. Chaque règle s’exprime comme une phrase claire et fluide, proche de la logique de la programmation fonctionnelle : on compose des conditions et des prédicats avec des méthodes enchaînées (that(), should(), resideInAPackage()), rendant la lecture naturelle même pour un œil extérieur. Cette approche favorise la compréhension immédiate des intentions architecturales.

L’architecture logicielle n’est pas figée : elle évolue au rythme de l’organisation et des besoins. ArchUnit soutient cette dynamique en instaurant une forme de contrat social autour du code. Les règles d’architecture deviennent explicites, discutables, révisables, et leur non-respect est immédiatement détecté.

Dans une démarche craft, cette pratique constitue une garantie essentielle : maintenir un code non seulement fonctionnel, mais aussi compréhensible et durable. Les tests d’architecture deviennent alors une manifestation tangible de la culture de qualité d’une équipe.

ArchUnit ne remplace ni la réflexion ni la conception : il les prolonge. Il agit comme un garant silencieux de la cohérence du projet, rappelant que chaque dépendance est un choix, et qu’un bon choix aujourd’hui évite des migrations coûteuses demain.


Conclusion : préserver la structure, renforcer la culture

Une architecture saine ne repose pas uniquement sur la conception initiale, mais sur la capacité à en vérifier continuellement la stabilité. ArchUnit, allié à une approche hexagonale rigoureuse, permet de concilier innovation, évolutivité et robustesse.

En intégrant la validation structurelle dans le cycle de vie du code, une équipe institutionnalise sa culture de qualité. Elle réduit les effets de dérive, renforce la compréhension collective et transforme la maintenance en activité maîtrisée.

Ainsi, le craft n’est plus une philosophie abstraite, mais une pratique mesurable et transmissible : ArchUnit en devient l’un des instruments les plus concrets.

Notes de Craft