Le problème
Dans beaucoup de projets, l’infrastructure finit par piloter le design.
On le voit vite :
- le modèle métier ressemble au schéma de base de données
- les cas d’usage sont organisés autour du framework
- les noms du domaine reprennent ceux d’un provider externe
- le code métier dépend directement d’un ORM, d’un SDK ou d’un format réseau
- une décision technique impose la structure de tout le système
Exemples fréquents :
- “on a modélisé ça comme ça parce que Prisma le rend simple”
- “ce service existe surtout parce que l’API Stripe fonctionne comme ça”
- “on a mis la règle ici parce qu’on l’avait déjà dans le controller”
- “cet objet correspond à une table, donc c’est forcément une entité métier”
Le problème n’est pas d’utiliser une base de données, un framework ou un provider externe.
Le problème est de laisser ces éléments décider à la place du métier.
Quand ça arrive :
- le modèle devient artificiel
- les changements techniques coûtent trop cher
- le cœur métier devient difficile à lire
- l’architecture se fige autour de détails volatils
L’idée importante est simple : la technique est nécessaire, mais elle ne doit pas être le centre du design.
L’idée simple
Dans une architecture hexagonale, l’infrastructure doit rester un détail.
Ça ne veut pas dire qu’elle est sans importance. Ça veut dire qu’elle ne doit pas dicter :
- les concepts métier
- les règles métier
- les frontières du domaine
- la structure des cas d’usage
Le cœur du système doit pouvoir exprimer :
- ce qu’il fait
- de quoi il a besoin
- quelles règles il protège
sans dépendre directement :
- d’un framework web
- d’un ORM
- d’une base SQL précise
- d’un provider de paiement
- d’un broker de messages
Autrement dit :
- le métier exprime l’intention
- l’infrastructure fournit une implémentation
- le design ne part pas des outils
Comment ça fonctionne
L’idée “infrastructure as detail” devient concrète quand on sépare clairement l’intention métier de son implémentation technique.
1. Le cœur exprime des besoins, pas des technologies
Le cœur peut avoir besoin de :
- persister une commande
- débiter un paiement
- envoyer une notification
- publier un événement
- charger un client
Mais il ne devrait pas dire :
- utiliser PostgreSQL
- appeler Stripe
- publier sur Kafka
- mapper un DTO Express
- manipuler un modèle Prisma
La différence est importante.
Le besoin métier est stable. La technologie qui y répond peut changer.
2. L’infrastructure implémente les besoins du cœur
L’infrastructure est là pour brancher le système au monde réel :
- HTTP
- base de données
- queue
- stockage
- service externe
- cache
- observabilité
Elle est nécessaire, mais elle doit rester au bord.
Son rôle :
- implémenter des ports
- traduire des formats
- gérer les protocoles
- absorber les contraintes techniques
- contenir les détails volatils
Elle ne doit pas définir le modèle du domaine.
3. Le design doit survivre aux changements techniques
Une bonne règle pratique est simple :
si remplacer une technologie oblige à réécrire la logique métier, alors l’infrastructure n’était pas un détail.
Exemples de changements qui devraient rester localisés :
- Express vers Fastify
- PostgreSQL vers DynamoDB
- Stripe vers Adyen
- Kafka vers SQS
- envoi d’email via Resend au lieu de SendGrid
Le cœur peut évoluer aussi, bien sûr. Mais il ne devrait pas dépendre structurellement de ces choix.
Schéma
Lecture utile :
- le cœur exprime des besoins via des ports
- l’infrastructure fournit les implémentations
- la technologie est branchée sur le système, pas l’inverse
Exemple concret
Prenons un cas simple : activer un abonnement.
Le système doit :
- charger un compte client
- vérifier que l’abonnement peut être activé
- enregistrer le nouvel état
- déclencher la facturation initiale
Mauvais design
import { PrismaClient } from "@prisma/client";
import Stripe from "stripe";
const prisma = new PrismaClient();
const stripe = new Stripe(process.env.STRIPE_API_KEY!);
export class ActivateSubscriptionService {
async execute(subscriptionId: string): Promise<void> {
const subscription = await prisma.subscription.findUnique({
where: { id: subscriptionId },
});
if (!subscription) {
throw new Error("Subscription not found");
}
if (subscription.status !== "pending") {
throw new Error("Subscription cannot be activated");
}
await stripe.paymentIntents.create({
amount: subscription.amount,
currency: subscription.currency,
customer: subscription.stripeCustomerId,
});
await prisma.subscription.update({
where: { id: subscription.id },
data: { status: "active" },
});
}
}
Ce code mélange :
- logique métier
- modèle de persistence
- détail Stripe
- dépendance ORM concrète
Le service est construit autour de l’infrastructure.
Design où l’infrastructure reste un détail
Ports
export interface SubscriptionRepository {
findById(subscriptionId: string): Promise<Subscription | null>;
save(subscription: Subscription): Promise<void>;
}
export interface BillingGateway {
createInitialCharge(subscription: Subscription): Promise<void>;
}
Domaine
export class Subscription {
constructor(
public readonly id: string,
private status: "pending" | "active" | "cancelled",
public readonly amount: number,
public readonly currency: string
) {}
activate(): void {
if (this.status !== "pending") {
throw new Error("Subscription cannot be activated");
}
this.status = "active";
}
}
Application
export class ActivateSubscriptionService {
constructor(
private readonly subscriptionRepository: SubscriptionRepository,
private readonly billingGateway: BillingGateway
) {}
async execute(subscriptionId: string): Promise<void> {
const subscription = await this.subscriptionRepository.findById(subscriptionId);
if (!subscription) {
throw new Error("Subscription not found");
}
subscription.activate();
await this.billingGateway.createInitialCharge(subscription);
await this.subscriptionRepository.save(subscription);
}
}
Adapter Prisma
export class PrismaSubscriptionRepository implements SubscriptionRepository {
async findById(subscriptionId: string): Promise<Subscription | null> {
// lecture ORM + mapping vers le domaine
return null;
}
async save(subscription: Subscription): Promise<void> {
// persistance ORM
}
}
Adapter Stripe
export class StripeBillingGateway implements BillingGateway {
async createInitialCharge(subscription: Subscription): Promise<void> {
// appel API Stripe
}
}
Ici, le cœur métier ne dépend pas de Prisma ni de Stripe. Le besoin reste stable. L’implémentation technique reste remplaçable.
Ce que ça change concrètement
Quand l’infrastructure reste un détail :
- le domaine devient plus lisible
- les use cases deviennent plus simples à tester
- les changements techniques sont plus localisés
- les intégrations externes contaminent moins le modèle
- les décisions de design partent du métier, pas de l’outil
Ce n’est pas une promesse de “switch provider en 5 minutes”. Ce n’est pas le sujet.
Le vrai gain est plus réaliste : le système encaisse mieux le changement.
Base de données comme détail
C’est souvent le point le plus contre-intuitif.
Dans beaucoup d’équipes, la base de données est perçue comme le centre du système. On part des tables, puis on en déduit les objets et les règles.
C’est souvent l’inverse qu’il faut viser :
- le domaine définit les concepts
- la persistence s’adapte à ces concepts
- le mapping absorbe l’écart
Ça ne veut pas dire ignorer les contraintes de persistence. Ça veut dire éviter que le schéma SQL décide seul du modèle métier.
Framework comme détail
Même logique pour le framework.
Un framework est utile pour :
- exposer des endpoints
- gérer l’injection
- fournir des middlewares
- câbler le runtime
Mais il ne devrait pas décider :
- ce qu’est une commande
- ce qu’est une entité métier
- où vivent les règles
- quelle dépendance a le droit d’entrer dans le domaine
Le framework aide à brancher l’application. Il ne remplace pas l’architecture.
Services externes comme détail
Un provider externe a presque toujours son propre modèle :
- ses noms
- ses contraintes
- ses statuts
- ses erreurs
- ses objets techniques
Le danger est de laisser ce modèle remonter dans le cœur.
Exemple :
- utiliser directement un
StripeCustomerdans le domaine - reprendre tel quel les statuts d’un provider dans les entités métier
- exposer des erreurs techniques externes au lieu de traduire l’intention
Le bon réflexe est de traduire au bord.
Le domaine parle son propre langage. L’adapter parle celui du provider.
Exemple de structure simple
src/
subscription/
domain/
subscription.ts
application/
activate-subscription/
activate-subscription.service.ts
subscription-repository.port.ts
billing-gateway.port.ts
infrastructure/
persistence/
prisma-subscription-repository.ts
billing/
stripe-billing-gateway.ts
http/
activate-subscription.controller.ts
Le point important n’est pas l’arborescence exacte. Le point important est de rendre visible :
- le cœur métier
- les ports
- les détails techniques
Points importants
- Infrastructure “as detail” ne veut pas dire infrastructure “sans importance”.
- Ça veut dire qu’elle ne doit pas piloter le design du métier.
- Le cœur exprime des besoins stables.
- L’infrastructure implémente ces besoins avec des technologies concrètes.
- Une base de données, un framework ou un provider externe restent des choix d’implémentation.
- Le bon design part du métier, pas du schéma SQL ni du SDK.
Erreurs fréquentes
Modéliser le domaine à partir de la base
On part des tables, puis on appelle “domaine” une projection du schéma. Le résultat est souvent pauvre côté métier et rigide côté évolution.
Utiliser directement les types techniques dans le cœur
Exemples :
- entités ORM
- types Express
- objets Stripe
- payloads Kafka bruts
Dès que ces types traversent trop loin, la frontière devient floue.
Confondre commodité locale et bon design global
Oui, utiliser directement le SDK ou l’ORM dans un use case peut sembler plus rapide. Mais ce confort local devient souvent un coût global quelques mois plus tard.
Croire qu’il faut rendre toute infrastructure interchangeable
Le but n’est pas de pouvoir tout remplacer à tout moment. Le but est de limiter le couplage là où il fait mal.
Certaines dépendances resteront très concrètes. Elles doivent simplement rester au bon endroit.
Traiter le mapping comme un problème secondaire
Le mapping entre domaine et infrastructure est souvent le prix normal d’une frontière saine. Le supprimer à tout prix pousse souvent la technique au centre du système.
Conclusion
L’infrastructure doit rester au service du système, pas l’inverse.
Base de données, framework et providers externes sont essentiels. Mais ce sont des détails d’implémentation autour d’un cœur qui doit rester lisible et stable.
Le réflexe utile à retenir est simple :
si une décision technique redessine le métier, l’infrastructure a pris trop de place.