Architecture hexagonale : comprendre une fois, réutiliser partout
Si ton code métier se retrouve collé à ton framework, à ta base de données ou à une API externe, tu finis vite avec un projet difficile à faire évoluer. L’architecture hexagonale propose une autre façon de découper une application : le cœur métier au centre, les dépendances autour, et des ports pour parler avec le monde extérieur.
Dans ce guide, je te montre comment elle fonctionne, pourquoi elle aide vraiment en maintenance, et comment la mettre en place dans un projet web avec TypeScript ou Python.
Pourquoi parler d’architecture hexagonale
On entend souvent parler d’architecture hexagonale, de clean architecture, de onion architecture. Les noms changent, l’idée reste proche : isoler la logique métier des détails techniques.
Le but est simple. Ton application doit pouvoir :
- changer de base de données sans réécrire tout le code métier ;
- remplacer une API tierce sans toucher aux règles métier ;
- tester la logique métier sans lancer un serveur HTTP ou une base PostgreSQL.
C’est exactement ce que l’architecture hexagonale rend plus facile.
Le principe central est simple : ton métier ne dépend pas de l’infrastructure.
Le framework, la base de données, le cache ou la file de messages sont des détails externes.
Le problème que résout vraiment ce pattern
Dans beaucoup de projets, on commence proprement puis ça glisse. Le code HTTP appelle directement la base. Les règles métier se retrouvent dans les handlers. Les requêtes SQL sont dispersées. Une logique de calcul est copiée dans trois fichiers.
Au bout d’un moment, un changement banal devient risqué.
Exemple typique :
- tu exposes une route
POST /orders; - le handler valide un peu les données ;
- il appelle directement le modèle ORM ;
- le modèle applique au passage une règle métier ;
- un test casse quand tu changes le schéma SQL.
Le problème n’est pas le framework. Le problème, c’est l’absence de frontière nette entre les responsabilités.
Le modèle mental : centre et périphérie
L’architecture hexagonale repose sur une idée visuelle simple. Au centre, tu mets le domaine métier. Autour, tu places les systèmes qui communiquent avec lui.
Le cœur métier
Le cœur contient ce qui donne du sens à ton application :
- les entités ;
- les objets de valeur ;
- les règles métier ;
- les cas d’usage.
Ce cœur ne doit pas savoir si les données viennent d’une API REST, d’un formulaire web ou d’une queue RabbitMQ.
Les ports
Un port est une interface. Il décrit une capacité attendue, sans dire comment elle est implémentée.
Exemples :
OrderRepositorypour lire et écrire des commandes ;PaymentGatewaypour encaisser un paiement ;EmailSenderpour envoyer un email.
Le cœur dépend du port, pas de l’implémentation.
Les adapters
Un adapter est l’implémentation concrète du port. Il fait le lien avec le monde extérieur :
- PostgreSQL ;
- Prisma, TypeORM ou SQLAlchemy ;
- Stripe ;
- un service email ;
- un contrôleur HTTP.
C’est là que les détails techniques vivent. Ils sont utiles, mais ils ne doivent pas contaminer le domaine.
| 3 | 1 | 2 |
|---|---|---|
| couches à séparer | logique métier à protéger | concepts à garder en tête |
Une structure simple à retenir
Tu peux voir une application hexagonale comme ça :
- le centre calcule ;
- les ports décrivent ;
- les adapters branchent le système sur l’extérieur.
Ce découpage évite une confusion fréquente : le framework n’est pas le métier. Un contrôleur Fastify ou Express n’est qu’une porte d’entrée. Une migration SQL n’est qu’un détail d’infrastructure. Un use case, lui, porte la règle.
Exemple de découpe
Imaginons une application de facturation :
- le cœur sait créer une facture ;
- un port permet de sauvegarder la facture ;
- un adapter PostgreSQL stocke les données ;
- un adapter HTTP reçoit la requête ;
- un adapter Stripe gère le paiement.
Le code métier reste stable même si tu changes l’un de ces éléments.
Ce que tu veux garder stable
Le cœur métier doit rester lisible, testable et indépendant. Si tu peux expliquer la logique de ton application sans parler du framework ni de la base de données, tu es sur la bonne voie.
Un premier exemple en TypeScript
Voici un exemple volontairement simple. L’objectif n’est pas de faire joli, mais de montrer les frontières.
typescript// Domaine
type Order = {
id: string;
amount: number;
paid: boolean;
};
interface OrderRepository {
save(order: Order): Promise<void>;
}
class CreateOrderUseCase {
constructor(private readonly repo: OrderRepository) {}
async execute(amount: number): Promise<Order> {
if (amount <= 0) {
throw new Error("Amount must be positive");
}
const order: Order = {
id: crypto.randomUUID(),
amount,
paid: false,
};
await this.repo.save(order);
return order;
}
}
Ce que fait ce code
Le type Order représente une donnée métier simple.OrderRepository est un port : il dit ce dont le cas d’usage a besoin, sans choisir la technologie.CreateOrderUseCase contient la règle métier. Il refuse un montant nul ou négatif, crée la commande, puis demande au repository de la sauvegarder.
Le point important, c’est que le use case ne connaît ni PostgreSQL, ni Prisma, ni Express. Il peut être testé avec un faux repository en mémoire.
Brancher un adapter concret
Le repository peut ensuite être implémenté avec PostgreSQL, MongoDB ou même un fichier local pour un prototype.
typescriptclass InMemoryOrderRepository implements OrderRepository {
private orders: Order[] = [];
async save(order: Order): Promise<void> {
this.orders.push(order);
}
}
Avec ça, tu peux tester ta logique sans infrastructure. Puis, plus tard, tu remplaces InMemoryOrderRepository par PostgresOrderRepository sans changer le use case.
C’est là que l’architecture hexagonale devient utile au quotidien : le changement reste local.
Même idée en Python
Python fonctionne très bien avec cette approche. Tu peux garder la même logique avec Protocol pour définir les ports.
pythonfrom dataclasses import dataclass
from typing import Protocol
import uuid
@dataclass
class Order:
id: str
amount: float
paid: bool = False
class OrderRepository(Protocol):
def save(self, order: Order) -> None:
...
class CreateOrderUseCase:
def __init__(self, repo: OrderRepository):
self.repo = repo
def execute(self, amount: float) -> Order:
if amount <= 0:
raise ValueError("Amount must be positive")
order = Order(
id=str(uuid.uuid4()),
amount=amount,
)
self.repo.save(order)
return order
Ce que fait ce code
Le Protocol joue le rôle du port. Il décrit l’interface attendue par le cas d’usage.
Le use case crée une Order, vérifie la donnée métier, puis appelle le repository.
Le code de domaine reste simple. Tu peux le tester avec un faux objet Python qui a une méthode save, sans base ni serveur web.
Quand l’architecture hexagonale vaut le coup
Tu n’as pas besoin de l’appliquer partout. Pour un script jetable ou une petite page statique, c’est souvent trop lourd.
En revanche, elle devient utile dès que tu as :
- plusieurs intégrations externes ;
- des règles métier qui évoluent ;
- des tests unitaires sérieux ;
- plusieurs interfaces d’entrée, par exemple API, batch, webhooks ;
- un projet qui doit vivre plus de quelques semaines.
Le bon réflexe : si ton métier change plus vite que ton infrastructure, sépare-les.
Hexagonale, clean architecture, onion : ce qu’il faut comprendre
Le débat autour des noms prend parfois trop de place. En pratique, ces approches ont beaucoup de points communs.
| Critère | Architecture hexagonale | Clean architecture |
|---|---|---|
| Vision | ports et adapters | couches concentriques |
| Objectif | isoler le domaine | isoler le domaine |
| Entrées/sorties | via ports | via interfaces et use cases |
| Style de code | orienté interactions | orienté dépendances |
Le détail change selon les écoles. Le fond reste le même : ton cœur métier ne doit pas dépendre de l’infrastructure.
Les erreurs que je vois souvent
La première erreur, c’est de faire une architecture hexagonale “sur le papier” seulement. Le dossier est bien rangé, mais les use cases appellent quand même directement l’ORM.
La deuxième erreur, c’est d’avoir trop d’abstraction trop tôt. Si ton app a deux fonctions et une table, tu peux rester simple. Le pattern ne sert pas à compliquer un projet qui va bien.
La troisième erreur, c’est de mettre toute la logique dans les adapters. Là, tu perds l’intérêt du modèle. Le code devient dur à tester, puis dur à faire évoluer.
Méthode simple pour commencer sur un vrai projet
Tu peux adopter l’architecture hexagonale sans tout réécrire.
1. Identifie le métier
Liste les actions principales de ton application. Pas les écrans, pas les routes. Les actions métier.
2. Définis les use cases
Chaque action importante devient un cas d’usage. Par exemple :
- créer une commande ;
- valider un paiement ;
- envoyer une relance ;
- générer un export.
3. Crée les ports
Pour chaque besoin externe, définis une interface. Base de données, email, paiement, stockage de fichiers, API tierce.
4. Branche les adapters
Ensuite seulement, tu relies les détails techniques. Le code HTTP appelle un use case. Le use case appelle un port. L’adapter fait le travail concret.
5. Teste le cœur sans infrastructure
C’est là que tu gagnes le plus. Tu testes les règles métier avec des doubles simples, sans réseau ni base de données.
Un bon test doit parler métier
Si ton test ressemble à une configuration de framework, il teste mal ton application. Un bon test vérifie une règle, une décision, un résultat. Le reste doit rester invisible.
Ce que tu gagnes concrètement
L’intérêt principal est très terre à terre : tu réduis l’effet domino quand tu modifies une partie du système.
Tu peux changer :
- le framework web ;
- le moteur de base de données ;
- le service de paiement ;
- la manière de lancer un traitement.
Le cœur métier, lui, bouge peu.
Tu gagnes aussi en lisibilité. Quand un nouveau développeur ouvre le projet, il comprend plus vite où vit la règle métier et où vit le détail technique. Ça évite beaucoup de chasse au trésor dans le code.
Tu veux structurer un projet web sans l’enfermer dans son framework ?
Je peux t’aider à poser une architecture claire, testable et prête à évoluer.
Conclusion
L’architecture hexagonale n’est pas une mode à coller sur un README. C’est une manière de penser un projet pour que la logique métier reste lisible et stable, même quand les outils autour changent.
Si tu développes une application qui va durer, commence par poser les frontières : cœur métier, ports, adapters. Tu n’as pas besoin de tout refaire d’un coup. Il suffit de prendre une première fonctionnalité, de la sortir du framework, puis de répéter.
Si tu veux aller plus loin, regarde comment tes cas d’usage sont organisés aujourd’hui. C’est souvent là que tout commence.