Concept #018
ArchitectureL'Architecture Hexagonale
- architecture
- domain-driven-design
- programmation
Le problème : quand la technique envahit le métier
Imaginez une application e-commerce. Au fil du temps, la logique de traitement des commandes se retrouve entrelacée avec des appels directs à PostgreSQL, des imports de SDKs Stripe, des références à des endpoints REST. Un jour, le client veut migrer vers MongoDB. Ou remplacer Stripe par Adyen. Ou exposer une API GraphQL en plus du REST.
Résultat : tout est à refaire. Parce que la logique métier — ce qui fait la vraie valeur de l'application — est prisonnière de décisions techniques qui, elles, changent en permanence.
C'est le couplage fort entre domaine et infrastructure. Et c'est exactement le problème que l'architecture hexagonale résout.
Le principe hexagonal : ports et adapters
Proposée par Alistair Cockburn en 2005, l'architecture hexagonale repose sur une idée simple mais puissante : le domaine métier ne doit jamais dépendre de la technique. C'est la technique qui s'adapte au domaine, pas l'inverse.
Pour y parvenir, elle introduit deux concepts clés :
- Les ports : des interfaces définies par le domaine. Ils expriment ce dont le domaine a besoin (un port primaire reçoit les commandes de l'extérieur ; un port secondaire expose les besoins vers l'infrastructure).
- Les adapters : des implémentations concrètes qui connectent le monde extérieur aux ports. Un adapter peut être une base de données, une API tierce, un système de fichiers, ou même un test.
La règle d'or : le code du domaine n'importe jamais une technologie concrète. Il ne connaît que des interfaces.
Schéma conceptuel
┌──────────────────────────────────────────┐
│ INFRASTRUCTURE │
│ │
│ [API REST] [CLI] [Scheduler] │
│ │ │ │ │
│ Adapter Adapter Adapter │
│ (in) (in) (in) │
│ └─────────┬───────┘ │
│ PORT (in) │
│ ┌────────┴────────┐ │
│ │ │ │
│ │ DOMAINE │ │
│ │ MÉTIER │ │
│ │ │ │
│ └────────┬────────┘ │
│ PORT (out) │
│ ┌─────────┴───────┐ │
│ Adapter Adapter Adapter │
│ (out) (out) (out) │
│ │ │ │ │
│ [PostgreSQL] [Stripe] [SendGrid] │
│ │
└──────────────────────────────────────────┘
Le domaine est au centre, ignorant tout de ce qui l'entoure. Il parle uniquement à travers ses ports.
Exemple TypeScript
Voici comment cela se traduit dans le code. On commence par définir le port — une interface pure, sans aucune dépendance technique :
// domain/ports/OrderRepository.ts
export interface OrderRepository {
findById(id: string): Promise<Order | null>;
save(order: Order): Promise<void>;
findByCustomerId(customerId: string): Promise<Order[]>;
}Le service métier ne connaît que ce contrat. Il est injecté, jamais instancié directement :
// domain/services/OrderService.ts
export class OrderService {
constructor(private readonly orders: OrderRepository) {}
async placeOrder(customerId: string, items: OrderItem[]): Promise<Order> {
const order = Order.create(customerId, items);
order.validate(); // logique métier pure
await this.orders.save(order);
return order;
}
}Ensuite, on écrit l'adapter qui implémente le port avec la technologie choisie :
// infrastructure/adapters/PostgresOrderRepository.ts
import { PrismaClient } from "@prisma/client";
import { OrderRepository } from "../../domain/ports/OrderRepository";
export class PostgresOrderRepository implements OrderRepository {
constructor(private readonly prisma: PrismaClient) {}
async findById(id: string): Promise<Order | null> {
const row = await this.prisma.order.findUnique({ where: { id } });
return row ? Order.fromPersistence(row) : null;
}
async save(order: Order): Promise<void> {
await this.prisma.order.upsert({
where: { id: order.id },
create: order.toPersistence(),
update: order.toPersistence(),
});
}
async findByCustomerId(customerId: string): Promise<Order[]> {
const rows = await this.prisma.order.findMany({ where: { customerId } });
return rows.map(Order.fromPersistence);
}
}Demain, si vous migrez vers MongoDB, vous écrivez MongoOrderRepository — le domaine, lui, ne change pas d'une ligne.
Le lien avec la Clean Architecture
L'architecture hexagonale et la Clean Architecture de Robert C. Martin partagent la même philosophie : la règle de dépendance. Les dépendances ne pointent que vers l'intérieur, jamais vers l'extérieur.
La Clean Architecture formalise davantage les couches (Entities, Use Cases, Interface Adapters, Frameworks), tandis que l'architecture hexagonale reste plus souple sur leur nombre. En pratique, les deux sont souvent combinées : on utilise la terminologie hexagonale (ports/adapters) avec l'organisation en couches de la Clean Architecture.
L'essentiel reste identique : le domaine est souverain, et tout le reste est un détail d'implémentation.
Les avantages pour les tests
C'est là que l'architecture hexagonale révèle tout son potentiel. Puisque le domaine ne dépend que d'interfaces, on peut le tester sans aucune infrastructure réelle.
Fini les bases de données de test, les mocks complexes de SDKs externes, les serveurs à démarrer. On remplace chaque adapter par un double de test :
// tests/doubles/InMemoryOrderRepository.ts
export class InMemoryOrderRepository implements OrderRepository {
private store = new Map<string, Order>();
async findById(id: string): Promise<Order | null> {
return this.store.get(id) ?? null;
}
async save(order: Order): Promise<void> {
this.store.set(order.id, order);
}
async findByCustomerId(customerId: string): Promise<Order[]> {
return [...this.store.values()].filter(
(o) => o.customerId === customerId
);
}
}Les tests sont rapides, déterministes, et testent exactement ce qui compte : la logique métier.
it("should create a valid order for a known customer", async () => {
const repository = new InMemoryOrderRepository();
const service = new OrderService(repository);
const order = await service.placeOrder("customer-42", [
{ productId: "prod-1", quantity: 2 },
]);
expect(order.status).toBe("pending");
expect(await repository.findById(order.id)).not.toBeNull();
});En résumé
L'architecture hexagonale, c'est une discipline de séparation : le quoi (le domaine) ne sait rien du comment (l'infrastructure). Les ports définissent les contrats, les adapters les implémentent. Le résultat : un code métier stable, testable, et libéré de la tyrannie des choix technologiques du moment.