Infrastructure as Detail

Pourquoi la base de données, le framework et les services externes doivent rester des détails techniques et non piloter le design du système.

← Retour au concept parent

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

Infrastructure comme détail

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 StripeCustomer dans 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.