Pourquoi je l'ai adoptée
Je n'ai pas choisi l'architecture hexagonale parce qu'elle était à la mode. Je l'ai choisie parce que le modèle de delivery avait un problème concret : la logique métier était coûteuse à faire évoluer et difficile à tester sans embarquer l'infrastructure avec elle.
Le problème n'était pas l'élégance architecturale. C'était le risque de delivery. Chaque changement métier dépendait d'une base de données, d'un mailer, d'un fournisseur de stockage ou d'un serveur HTTP dans le chemin de test. Le domaine était couplé à son infrastructure, et chaque modification portait un coût invisible.
Ports and Adapters propose une séparation claire : distinguer ce que l'application fait de la manière dont elle le fait. Le domaine exprime les règles métier. L'infrastructure les délivre. Rien dans le domaine ne devrait savoir s'il parle à PostgreSQL, S3, un fournisseur d'email ou un double de test.
J'ai appliqué cette approche sur deux missions consécutives pendant dix-huit mois : une plateforme d'authentification Node.js pour un groupe bancaire, puis un SaaS de gestion de flotte construit de zéro.
Voici ce qui a tenu, ce qui a créé de la friction, et ce que j'imposerais plus tôt la prochaine fois.
Ce qui a réellement fonctionné
Changer d'infrastructure sans toucher au domaine
C'est là que l'architecture a prouvé sa valeur.
Sur les deux missions, le fournisseur de stockage de fichiers et le fournisseur d'email ont changé plusieurs fois. Fournisseurs différents, API différentes, contraintes opérationnelles différentes. Chaque changement est resté dans la couche adaptateur. Le domaine n'a pas bougé. Les use cases n'ont pas bougé. Les tests métier n'ont pas changé.
Quand un changement de fournisseur se fait derrière un port et que la suite de tests domaine passe sans modification, le pattern cesse d'être théorique.
Les règles métier sont restées là où l'équipe pouvait les raisonner
Dans une architecture couplée, les règles se dispersent. Certaines vivent dans des services, d'autres fuient dans les contrôleurs, d'autres se cachent dans des requêtes SQL, et certaines ne sont appliquées que par la validation près du transport.
Avec l'architecture hexagonale, la règle est devenue plus stricte : les invariants métier appartiennent au domaine. Les entities et value objects acceptent un état valide ou le rejettent. Les aggregates acceptent une opération ou retournent une erreur domaine. La couche application orchestre le use case, mais elle n'introduit pas de décision de persistance ou de transport dans la règle elle-même.
Cela a rendu les revues plus claires. Une pull request qui change une règle métier devait toucher le modèle domaine ou le use case. Une pull request qui change une API fournisseur devait rester dans l'infrastructure.
La séparation n'était pas cosmétique. Elle donnait à l'équipe une question simple en review : sommes-nous en train de changer le métier, ou la manière dont le métier est délivré ?
Les tests sont devenus plus rapides et plus utiles
Le gain le plus important est venu des adaptateurs en mémoire.
Avant d'implémenter un vrai adaptateur, l'équipe pouvait définir le port, créer une implémentation en mémoire et couvrir les cas métier contre le use case. Les tests étaient rapides, déterministes, et indépendants de l'état de la base, de la latence réseau ou des API tierces.
Le workflow devenait :
- Définir le port dont le use case a besoin.
- Implémenter un adaptateur en mémoire pour les tests.
- Couvrir les scénarios métier.
- Implémenter le vrai adaptateur infrastructure.
- Ajouter une surface plus réduite de tests d'intégration pour l'adaptateur lui-même.
Au moment où la base de données ou le fournisseur externe entraient en jeu, le comportement métier était déjà prouvé. Les tests d'infrastructure restaient importants, mais ils devenaient ciblés au lieu d'être le seul moyen de tester l'application.
Le même use case pouvait être exposé par plusieurs interfaces
Comme les use cases étaient des classes applicatives sans dépendance framework, le même comportement pouvait être exposé en REST, en tRPC, par une CLI ou via une REPL.
Cela comptait dans le delivery quotidien. Pendant le développement, appeler un use case directement depuis une REPL accélérait le debug. Pendant les tests, piloter le comportement par les ports applicatifs gardait les tests centrés sur l'application plutôt que sur le transport.
Cela gardait aussi l'équipe honnête. Si un use case ne fonctionne que lorsqu'il est appelé par un contrôleur d'un framework précis, ce n'est pas vraiment un use case. C'est du code framework avec un nom métier.
La gestion d'erreur est devenue plus lisible
Les erreurs domaine et les erreurs infrastructure ont cessé de se mélanger.
Une erreur domaine signifie qu'une règle métier a été violée. Elle a un nom, un sens, et appartient au vocabulaire métier. Une erreur infrastructure signifie qu'un élément externe a échoué : timeout réseau, problème de connexion à la base, erreur du fournisseur de stockage, ou API tierce qui répond de manière inattendue.
Quand ces deux familles sont séparées, la gestion d'erreur devient intentionnelle. On arrête d'écrire des catch blocks qui ne savent pas ce qu'ils attrapent. On arrête d'exposer des stack traces d'infrastructure comme des échecs métier. Chaque erreur indique où regarder.
Le langage ubiquitaire a payé au-delà du code
L'architecture a aussi changé les conversations.
Quand le code est organisé autour de concepts métier plutôt qu'autour de modules framework ou de tables de base de données, les discussions produit et métier se projettent plus naturellement dans l'implémentation. Un product manager peut décrire une règle, et l'équipe peut la placer dans le modèle.
C'est un bénéfice sous-estimé de cette approche. L'architecture hexagonale n'est pas seulement une technique de test. Associée au DDD, elle renforce le lien entre langage produit, frontières d'architecture et ownership du code.
Ce qui a été plus difficile que prévu
Le boilerplate est réel
L'architecture hexagonale a un coût. Ports, adaptateurs, use cases, domain services, value objects et presenters créent plus de fichiers qu'une structure en couches classique.
Sur une petite feature, l'overhead est visible. Sur un produit plus large, il se rembourse souvent par des frontières plus claires, des tests plus rapides et des changements d'intégration plus sûrs. Mais l'investissement initial n'est pas imaginaire, et une équipe ne devrait pas prétendre le contraire.
La question n'est pas de savoir si le pattern ajoute de la structure. Il en ajoute. La question est de savoir si le produit a assez de règles métier durables et d'intégrations changeantes pour justifier cette structure.
L'alignement d'équipe n'est pas automatique
C'était la partie la plus difficile.
Chaque personne avait un modèle mental légèrement différent de ce que voulait dire architecture hexagonale en pratique. Où vit la validation ? Est-ce qu'un repository est un port ou un adaptateur ? Est-ce qu'un use case peut appeler un autre use case ? Un objet domaine doit-il lever une exception, retourner un Result, ou accumuler des violations ?
Sans conventions explicites, l'implémentation diverge. Et la divergence coûte cher dans une architecture qui dépend de la cohérence.
Nous avons eu certaines de ces conversations trop tard. Elles auraient dû avoir lieu avant la première feature.
Le problème de dérive de frontière framework
Les frameworks sont utiles. Ils doivent aussi rester à la périphérie.
Sur la mission de plateforme bancaire, deux patterns ont montré que la frontière commençait à dériver.
Le premier était la présence de décorateurs framework sur les use cases. Un use case est un objet de couche application. Il ne devrait pas avoir besoin d'une dépendance framework pour exister, pour être construit dans un test, ou pour être appelé par un autre adaptateur. Dès qu'un use case dépend d'un container framework, la couche application devient plus difficile à réutiliser et à tester isolément.
Le second était la présence de décorateurs ORM ou framework sur les entities domaine. Les entities sont censées exprimer des concepts métier et protéger les invariants. Quand elles portent de la metadata de mapping base de données, elles deviennent des artefacts de persistance déguisés en objets domaine.
NestJS était le framework concret dans ce cas, mais le sujet n'est pas spécifique à NestJS. La même dérive peut arriver avec n'importe quel framework ou ORM qui rend pratique l'annotation du modèle coeur.
La correction est une règle de frontière, pas un outil :
Le code framework vit à la périphérie.
Le code domaine et application reste du TypeScript pur.
Une frontière propre ressemblait à ceci :
src/
domain/
user/
user.ts
user-errors.ts
application/
user/
create-user.ts
ports.ts
infrastructure/
nestjs/
user.module.ts
user.controller.ts
persistence/
user.repository.ts
L'important n'est pas le nom des dossiers. C'est la direction des dépendances : l'infrastructure peut connaître l'application, l'application peut connaître le domaine, mais le domaine ne sait pas que le framework existe.
Ce que je ferais différemment
Écrire les conventions avant d'écrire le code
La prochaine fois que j'introduis cette architecture dans une équipe, j'écris l'ADR d'abord.
Pas une page wiki. Pas un schéma dans un slide deck. Un court Architecture Decision Record commité dans le repository et relu comme du code.
L'ADR minimum doit répondre à ces questions :
- Qu'est-ce qu'un port dans cette codebase ?
- Qu'est-ce qu'un adaptateur ?
- Où vit la validation d'input ?
- Où vivent les invariants métier ?
- Les use cases peuvent-ils appeler d'autres use cases ?
- Comment les erreurs domaine sont-elles représentées ?
- Comment les erreurs infrastructure sont-elles représentées ?
- Quels imports sont interdits entre les couches ?
- Où a lieu l'injection de dépendance ?
- Quels tests prouvent que la frontière fonctionne ?
Ce document n'a pas besoin d'être long. Il doit être explicite. Sans lui, l'architecture repose sur la mémoire et les préférences individuelles. Cela ne passe pas à l'échelle d'une équipe de delivery.
Faire respecter la frontière dès le premier jour
La dérive de frontière est plus facile à prévenir qu'à retirer.
Le mécanisme de contrôle peut rester simple : règles de lint, restrictions d'import, conventions de dossiers ou checklist de review. Le point important est que la frontière ne soit pas traitée comme une suggestion.
Par exemple :
domain/ne peut pas importer depuisinfrastructure/.application/ne peut pas importer de packages framework web.- les entities domaine ne portent pas de décorateurs ORM.
- les adaptateurs traduisent les préoccupations externes en commandes applicatives.
- les presenters traduisent les résultats applicatifs en réponses transport.
Ces règles ne sont pas de la bureaucratie. C'est ce qui permet à l'architecture de rester utile quand la pression delivery augmente.
Est-ce que cela vaut le coût ?
Oui, quand le produit porte des règles métier durables, plusieurs intégrations, et une équipe prête à faire respecter la frontière.
L'investissement initial est réel. On le ressent au premier sprint. Mais après quelques mois, les bénéfices se composent : les changements de fournisseur restent contenus, les tests métier tournent sans infrastructure, les conversations produit utilisent le même vocabulaire que le code, et les erreurs indiquent si l'échec vient d'une règle métier ou d'un système externe.
Ce n'est pas le bon défaut pour tous les projets. Un prototype, un script ou un service court terme n'en ont pas toujours besoin. Dans ces contextes, la cérémonie peut dépasser le bénéfice.
Mais pour un produit qui va évoluer, avec des intégrations qui vont changer et des règles métier qui comptent, le domaine mérite d'être protégé des décisions d'infrastructure.
Construisez la frontière. Écrivez les règles. Faites-les respecter tôt.
Le framework est un mécanisme de delivery, pas l'architecture.