Contextes délimités

VoletStratégique Le concept central de DDD. Si une seule notion doit être retenue de tout ce corpus, c’est celle-ci.

Un contexte délimité est une frontière explicite à l’intérieur de laquelle un modèle s’applique et chaque terme a exactement un sens. En dehors, le modèle ne prétend rien. Les mêmes mots peuvent y désigner autre chose, et c’est très bien.

Le problème qu’il résout

Prenez le mot « client » dans une entreprise de taille moyenne, et posez la question à trois services.

ServiceCe qu’est un clientCe qui l’intéresse
CommerceUn prospect, une opportunitéHistorique des échanges, potentiel, appétence
FacturationUne entité juridiqueRaison sociale, TVA intracommunautaire, IBAN, encours
SupportUn utilisateurIncidents, contrat de service, matériel installé

Aucun n’a tort. Le réflexe habituel est pourtant de construire une classe Client qui satisfasse les trois — le fameux modèle de données d’entreprise unifié. Le résultat est prévisible et se reconnaît partout :

  • quarante-cinq champs, dont trente sont null pour tout usage donné ;
  • des champs dont la signification dépend d’un autre champ (« l’IBAN n’est renseigné que si le client est facturable, sauf pour les grands comptes ») ;
  • une classe que personne ne peut modifier sans risquer de casser trois services ;
  • et un vocabulaire qui redevient ambigu, puisque le mot recouvre à nouveau trois réalités.

La solution de DDD est contre-intuitive et libératrice : arrêter de chercher un modèle unique. Trois contextes, trois modèles de client, reliés par la seule chose qu’ils ont réellement en commun — une identité.

​
// ---- Contexte Commerce ----
export class Prospect {
  constructor(
    readonly id: ClientId,                    // La seule chose partagée
    readonly raisonSociale: string,
    readonly appetence: ScoreAppetence,
    readonly dernierEchange: Date,
  ) {}
}

// ---- Contexte Facturation ----
export class Debiteur {
  constructor(
    readonly id: ClientId,                    // Le même identifiant
    readonly identiteJuridique: Siren,
    readonly tvaIntra: NumeroTva,
    readonly encours: Montant,
    readonly plafond: Montant,
  ) {}

  peutCommander(montant: Montant): boolean {
    return this.encours.plus(montant).inferieurA(this.plafond);
  }
}

// ---- Contexte Support ----
export class Beneficiaire {
  constructor(
    readonly id: ClientId,
    readonly niveauDeService: NiveauDeService,
    readonly incidentsOuverts: number,
  ) {}
}
# ---- Contexte Commerce ----
from dataclasses import dataclass


@dataclass(frozen=True)
class Prospect:
    id: ClientId                    # La seule chose partagée
    raison_sociale: str
    appetence: ScoreAppetence
    dernier_echange: date


# ---- Contexte Facturation ----
@dataclass(frozen=True)
class Debiteur:
    id: ClientId                    # Le même identifiant
    identite_juridique: Siren
    tva_intra: NumeroTva
    encours: Montant
    plafond: Montant

    def peut_commander(self, montant: Montant) -> bool:
        return (self.encours + montant) < self.plafond


# ---- Contexte Support ----
@dataclass(frozen=True)
class Beneficiaire:
    id: ClientId
    niveau_de_service: NiveauDeService
    incidents_ouverts: int
// ---- Contexte Commerce (crate `commerce`) ----
pub struct Prospect {
    pub id: ClientId,                     // La seule chose partagée
    pub raison_sociale: String,
    pub appetence: ScoreAppetence,
    pub dernier_echange: NaiveDate,
}

// ---- Contexte Facturation (crate `facturation`) ----
pub struct Debiteur {
    pub id: ClientId,                     // Le même identifiant
    pub identite_juridique: Siren,
    pub tva_intra: NumeroTva,
    encours: Montant,
    plafond: Montant,
}

impl Debiteur {
    pub fn peut_commander(&self, montant: Montant) -> bool {
        self.encours + montant < self.plafond
    }
}

// ---- Contexte Support (crate `support`) ----
pub struct Beneficiaire {
    pub id: ClientId,
    pub niveau_de_service: NiveauDeService,
    pub incidents_ouverts: u32,
}

Notez ce qui a disparu : le champ inutilisé, le null conditionnel, le commentaire d’exception. Et ce qui est apparu : trois noms différents. Prospect, Debiteur, Beneficiaire ne sont pas des synonymes de « client » — ce sont les mots que chaque service emploie réellement.

Tip

La duplication apparente inquiète toujours. Ce n’est pas de la duplication : ce sont trois concepts différents qui partagent un identifiant. La preuve opérationnelle : ils ne changent pas pour les mêmes raisons ni au même rythme. Une nouvelle règle de scoring commercial ne touche pas la facturation. C’est exactement le critère de la responsabilité unique, appliqué à l’échelle d’un modèle.

La frontière est d’abord linguistique

On ne trace pas un contexte selon des critères techniques. On le trace là où le langage change. Les trois signaux décrits dans Langage omniprésent sont les outils de détection : un mot qui a besoin d’un adjectif, deux mots pour une chose, un mot qui déclenche deux réactions.

D’autres indices convergent en général avec le premier :

  • Le rythme de changement. Deux parties qui évoluent à des cadences très différentes appartiennent probablement à deux contextes.
  • Les personnes. Si vous devez interroger deux experts distincts qui ne se parlent pas, la frontière est déjà là dans l’organisation.
  • Les règles de cohérence. Deux ensembles de données qui n’ont jamais besoin d’être cohérents dans la même transaction peuvent être séparés.
  • Les données réellement échangées. Si le contexte B n’a besoin que de trois champs sur quarante de A, il n’a pas besoin du modèle de A.

Ce qui constitue vraiment un contexte

Un contexte délimité n’est pas seulement une idée : c’est une frontière qu’on doit pouvoir constater. En pratique, il possède en propre :

ÉlémentExigence
Un modèleUn ensemble cohérent d’agrégats et d’objets valeur
Un langageUn vocabulaire sans ambiguïté interne
Une base de donnéesNon partagée — c’est le point le plus violé
Un code sourceUn module, un paquet, une crate, un dépôt
Une équipeIdéalement une seule ; jamais un morceau partagé entre trois
Warning

Deux contextes qui partagent des tables ne sont pas deux contextes. La base de données partagée est le mécanisme le plus efficace pour annuler tout le bénéfice du découpage : le schéma devient une interface implicite que personne ne peut faire évoluer, et le modèle de chacun se met à dépendre de celui des autres sans qu’aucun import ne le montre. Si la séparation physique est impossible à court terme, séparer les schémas — ou au minimum interdire les jointures inter-contextes — est le compromis minimal défendable.

Contexte délimité et microservice

C’est la confusion la plus répandue et la plus coûteuse.

Contexte délimitéMicroservice
Nature de la décisionModélisationDéploiement
Question poséeOù le modèle cesse-t-il d’être valide ?Que déploie-t-on séparément ?
Conséquence si on se trompeModèle ambiguLatence, pannes partielles, transactions distribuées

Les deux notions sont indépendantes, et les combinaisons possibles sont toutes légitimes :

  • Un contexte, un module dans un monolithe. Le point de départ recommandé — le monolithe modulaire. Toutes les frontières de modélisation, aucun coût réseau.
  • Un contexte, un service. Le cas dont tout le monde parle. Justifié par des besoins d’échelle, de cadence de déploiement ou d’autonomie d’équipe — pas par la modélisation.
  • Plusieurs contextes dans un service. Fréquent et souvent sain, au début.
  • Plusieurs services pour un contexte. Presque toujours une erreur : les services se partagent alors un modèle et un invariant, et l’on obtient un monolithe distribué — le couplage du monolithe avec les pannes du distribué.
Note

La règle pratique qui évite le plus de dégâts : découper en contextes d’abord, en services plus tard. Les frontières de modélisation sont difficiles à trouver et faciles à déplacer dans un monolithe ; elles deviennent presque impossibles à déplacer une fois qu’un appel réseau et un contrat d’API les traversent. Un contexte mal placé dans un monolithe est un refactoring ; un contexte mal placé entre deux services est un projet.

Quelle taille ?

Il n’existe pas de bonne réponse chiffrée, mais deux échecs symétriques.

Trop grand : le langage redevient ambigu à l’intérieur, ce qui est précisément ce qu’on voulait éviter. Le symptôme est le retour des adjectifs de désambiguïsation dans les conversations de l’équipe.

Trop petit : les invariants métier se retrouvent à cheval sur deux contextes, donc sur deux transactions. On se met à avoir besoin de cohérence immédiate entre des choses séparées, c’est-à-dire de transactions distribuées — le signe le plus sûr d’une frontière mal placée.

L’heuristique la plus utilisable : un contexte doit tenir dans la tête d’une équipe, et son modèle doit pouvoir être expliqué à un nouvel arrivant en une séance. Si l’explication nécessite « alors là, ça dépend du cas », le contexte est probablement trop large.

Le même objet dans deux contextes

Une dernière subtilité qui met du temps à passer. Une même chose du monde réel — une personne, un colis, un contrat — peut être modélisée dans plusieurs contextes, avec des attributs différents, un comportement différent, et parfois un cycle de vie différent.

Ce ne sont pas des copies à synchroniser : ce sont des points de vue. Le seul lien est l’identité. Le contexte Expédition n’a pas besoin de connaître le plafond de crédit ; le contexte Facturation n’a pas besoin de connaître le poids du colis. Chacun garde ce dont il a besoin pour décider, et rien de plus.

La question qui suit naturellement — comment ces contextes se procurent-ils ce dont ils ont besoin, et qui dépend de qui — est celle de la cartographie de contextes.

Références