Domain-Driven Design
Chapitre 3 / 10 · 12 min de lecture

Les contextes délimités (Bounded Contexts)

Un même mot ne signifie pas la même chose partout : délimiter explicitement la zone de validité de chaque modèle.

Sur un grand projet, plusieurs équipes finissent toujours par modéliser le même domaine — et, sans s'en rendre compte, par modéliser des choses différentes sous les mêmes mots. Un jour, l'équipe facturation ajoute un attribut à une classe Charge partagée ; quelques jours plus tard, l'application de paiement des fournisseurs se met à planter sur des enregistrements qui n'ont aucun sens. Les deux équipes croyaient parler de la même chose. Elles parlaient en réalité de deux concepts distincts qui se ressemblaient juste assez pour passer la compilation.

Eric Evans tire de cette anecdote l'un des enseignements les plus structurants du livre : viser un seul modèle unifié pour toute l'entreprise n'est ni réaliste ni rentable. La bonne stratégie n'est pas de tout fusionner, mais de délimiter explicitement la zone à l'intérieur de laquelle un modèle — et le langage qui l'accompagne — reste cohérent. C'est le rôle du contexte délimité (Bounded Context). Les exemples du livre sont en Java ou conceptuels ; nous les traduisons en TypeScript idiomatique, dans un univers e-commerce et logistique maritime.

Le piège du modèle unique

L'exigence la plus fondamentale d'un modèle, c'est sa cohérence interne : que chaque terme y ait toujours le même sens et qu'aucune règle n'en contredise une autre. Evans appelle cette propriété l'unification. Un modèle qui n'est pas logiquement cohérent ne veut tout simplement rien dire.

Dans un monde idéal, on aurait un unique modèle couvrant tout le domaine de l'entreprise, sans aucune définition contradictoire ou redondante. Mais le développement de grands systèmes n'a rien d'idéal. Maintenir ce niveau d'unification à l'échelle d'une entreprise entière coûte plus cher que ce que cela rapporte. Evans liste les risques d'une telle ambition :

  • on tente de remplacer trop d'existant à la fois ;
  • les gros projets s'enlisent parce que le coût de coordination dépasse les capacités de l'équipe ;
  • les applications aux besoins spécialisés héritent d'un modèle qui ne les satisfait pas vraiment, et doivent placer leur logique ailleurs ;
  • à l'inverse, vouloir contenter tout le monde produit un modèle bourré d'options qui le rendent inutilisable.

Vaughn Vernon reprend un nom imagé bien connu pour ce résultat : la grosse boule de boue (Big Ball of Mud), terme dû à Foote et Yoder. Un système monolithique où plusieurs modèles enchevêtrés cohabitent sans frontière explicite, où des concepts sans rapport sont éparpillés dans des modules interconnectés par des éléments contradictoires. Le langage de l'équipe y devient un dialecte flou que personne ne parle vraiment.

Attention

Les divergences de modèle ne viennent pas que de la technique. Elles naissent aussi de l'organisation des équipes, de la géographie des bureaux, des priorités managériales. Même quand rien ne s'oppose techniquement à l'intégration, le projet se retrouvera avec plusieurs modèles. Autant les reconnaître et les piloter plutôt que les subir.

Le contexte délimité

Plutôt que de lutter, on assume. Définissez explicitement le contexte dans lequel un modèle s'applique. Posez la frontière en termes d'organisation d'équipe, d'usage dans des parties précises de l'application, et de manifestations physiques comme les dépôts de code et les schémas de base de données. À l'intérieur de ces limites, gardez le modèle strictement cohérent — et ne vous laissez pas distraire par ce qui se passe dehors.

C'est cela, un contexte délimité (Bounded Context) : une frontière sémantique explicite à l'intérieur de laquelle un modèle et son langage ubiquitaire (Ubiquitous Language) sont unifiés. Dedans, chaque composant a un sens précis et fait des choses précises. Dehors, d'autres modèles s'appliquent, avec une terminologie, des concepts et des règles différents — un autre dialecte du langage ubiquitaire.

Note

Vernon en propose une belle métaphore : pensez aux nations d'Europe. En Allemagne, en France, en Italie, la langue officielle est certaine ; dès qu'on franchit la frontière, elle change. Un Bounded Context est une frontière de langue. La forme écrite la plus notable de cette langue, c'est le code source du modèle.

En traçant cette frontière, on obtient deux gains symétriques, selon Evans. Pour les équipes dans le contexte : la clarté — elles savent qu'elles doivent rester cohérentes avec un seul modèle, et conçoivent en conséquence. Pour les équipes hors du contexte : la liberté — elles n'ont plus à marcher dans une zone grise où elles utilisent un modèle différent tout en ayant vaguement le sentiment qu'elles devraient s'aligner.

« Client » n'est pas « Client »

Prenons le mot le plus innocent d'une application e-commerce : Client. Il paraît évident qu'il désigne « la même chose » partout. C'est précisément le piège — ce qu'Evans appelle un faux ami (false cognate) : deux personnes emploient le même mot en croyant parler de la même chose, alors que non.

Observons trois équipes. Pour les Ventes, un client est un prospect avec un panier, un segment marketing, une probabilité de conversion. Pour le Support, c'est un titulaire de tickets, avec un niveau de SLA et un historique d'incidents. Pour la Facturation, c'est une entité légale avec une adresse de facturation, un numéro de TVA et un encours. Mêmes mots, attributs et comportements radicalement différents.

// ❌ Avant : une classe Client fourre-tout, qui sert tout le monde.
class Client {
	constructor(
		public id: string,
		public nom: string,
		public email: string,
		// Ventes
		public segmentMarketing: string,
		public scoreConversion: number,
		// Support
		public niveauSla: "standard" | "premium",
		public ticketsOuverts: Ticket[],
		// Facturation
		public adresseFacturation: Adresse,
		public encoursAutorise: Money,
		public panierEnCours?: Panier,
		public numeroTva?: string,
		// ... et ça continue de grossir à chaque sprint.
	) {}
}

Cette classe est un faux ami institutionnalisé. Le champ encoursAutorise n'a aucun sens pour l'équipe Ventes, mais elle doit composer avec à chaque chargement. Toute modification de l'équipe Facturation risque de casser le Support. Et le langage se brouille : quand quelqu'un dit « le client est bloqué », parle-t-il d'un blocage de crédit, d'un compte support suspendu, ou d'un parcours d'achat interrompu ?

La réponse DDD n'est pas de préfixer (ClientVentes, ClientFacturation). C'est de donner à chaque contexte son propre Client, dans son propre module, avec uniquement ce dont il a besoin. Le nom du contexte se charge du cadrage ; à l'intérieur, le nom reste simplement Client.

// ✅ Après : un type Client par contexte, taillé pour ses besoins.

// ventes/Client.ts — contexte Ventes
export class Client {
	constructor(
		readonly id: ClientId,
		readonly segmentMarketing: string,
		private panier: Panier,
	) {}

	ajouterAuPanier(article: Article): void {
		this.panier.ajouter(article);
	}

	estProspectChaud(): boolean {
		return this.panier.valeur().superieureA(SEUIL_RELANCE);
	}
}
// ✅ facturation/Client.ts — contexte Facturation
export class Client {
	constructor(
		readonly id: ClientId,
		readonly numeroTva: NumeroTva | null,
		readonly adresseFacturation: Adresse,
		private encoursAutorise: Money,
		private encoursActuel: Money,
	) {}

	peutCommanderPour(montant: Money): boolean {
		return this.encoursActuel.plus(montant)
			.inferieureOuEgaleA(this.encoursAutorise);
	}
}

Chaque Client est petit, focalisé, et impossible à corrompre par les besoins d'un autre service. Le seul élément partagé est l'identifiant ClientId, qui sert de point de jonction quand les contextes doivent dialoguer.

Astuce

Quand un même nom porte plusieurs significations métier, ne cherchez pas à les réconcilier dans une classe « canonique ». Admettez la différence et créez autant de contextes que de significations. Trois sens pour « police d'assurance » (souscription, sinistres, inspection) ? Alors trois Bounded Contexts, chacun avec sa propre Police.

La frontière est concrète, pas conceptuelle

Un contexte délimité n'est pas qu'une idée de modélisation : il a des manifestations physiques. Evans insiste pour qu'on fixe la frontière à plusieurs niveaux simultanément.

DimensionManifestation du Bounded Context
OrganisationUne équipe travaille sur un seul contexte.
CodeUn module / dépôt / service par contexte.
DonnéesUn schéma de base de données dédié.
LangageUn dialecte cohérent du langage ubiquitaire.

Vernon est catégorique sur le couplage équipe ↔ contexte : une équipe par Bounded Context, et un dépôt de code séparé pour chacun. Une même équipe peut tenir plusieurs contextes, mais plusieurs équipes ne devraient jamais travailler sur un seul. C'est ce qui élimine les mauvaises surprises du type « une autre équipe a modifié notre code ». Votre équipe possède son code, son schéma, et définit les interfaces officielles par lesquelles son contexte doit être utilisé.

À retenir

Ne confondez pas Bounded Context et simple module. Mettre deux modèles dans deux modules leur donne certes des espaces de noms distincts qui évitent les collisions de noms à la compilation. Mais ce n'est qu'un mécanisme d'implémentation. Le vrai sujet le précède : reconnaître que les modèles diffèrent, et décider quoi en faire. Pire, des espaces de noms séparés peuvent masquer des divergences accidentelles. L'outil technique n'a jamais résolu un problème conceptuel.

Maintenir l'intégrité à l'intérieur

Tracer la frontière ne suffit pas : il faut garder le modèle sain à l'intérieur. Dès que plusieurs personnes travaillent dans un même contexte, le modèle a une forte tendance à se fragmenter. Quelqu'un ne comprend pas l'intention d'un objet et le modifie au point de le rendre inutilisable pour son usage initial ; quelqu'un d'autre ignore qu'un concept existe déjà et le duplique, de façon légèrement différente. Plus l'équipe est grande, plus le risque est élevé — mais trois ou quatre personnes suffisent à créer de vrais problèmes.

Evans recommande pour cela l'intégration continue (Continuous Integration), qu'il fait opérer à deux niveaux complémentaires.

Au niveau des concepts : une communication constante entre les membres de l'équipe, qui forge sans relâche le langage ubiquitaire et entretient une compréhension partagée du modèle qui évolue. C'est le plus fondamental.

Au niveau de l'implémentation : un processus systématique de fusion / build / test qui expose tôt les fractures. Les symptômes les plus parlants sont les tests automatisés qui échouent et — signe avant-coureur le plus précoce — la confusion du langage.

// Test de cohérence interne au contexte Facturation :
// le langage ("encours", "bloquer") doit rester sans ambiguïté.
describe("Client (contexte Facturation)", () => {
	it("refuse une commande au-delà de l'encours autorisé", () => {
		const client = new Client(
			ClientId.de("c-42"),
			NumeroTva.de("FR123"),
			adresseParis,
			Money.euros(1000), // encours autorisé
			Money.euros(900), // encours actuel
		);

		expect(client.peutCommanderPour(Money.euros(50))).toBe(true);
		expect(client.peutCommanderPour(Money.euros(200))).toBe(false);
	});
});

Evans repère deux symptômes de fragmentation : la duplication de concepts (deux éléments représentant la même idée, qu'il faut synchroniser à la main à chaque évolution) et les faux amis (le même terme pour deux choses différentes), plus insidieux car ils font que les équipes se marchent sur le code et que les bases de données se contredisent.

Piège courant

L'intégration continue n'est essentielle qu'à l'intérieur d'un Bounded Context. Les questions de conception entre contextes voisins — la traduction d'un modèle à l'autre — n'ont pas à être traitées au même rythme effréné. Ne rendez pas le travail plus lourd qu'il ne doit l'être.

Bounded Context et langage ubiquitaire

Le contexte délimité et le langage ubiquitaire sont les deux faces d'une même pièce. Le langage ubiquitaire n'a de sens qu'à l'intérieur d'une frontière : il n'existe pas de langage universel pour toute l'entreprise, mais un dialecte cohérent par contexte.

Concrètement, cela veut dire que le même mot peut — et doit — être défini différemment d'un contexte à l'autre, sans que ce soit une erreur. Dans le contexte Ventes, « valider » un client signifie qualifier un prospect ; dans Facturation, cela signifie vérifier sa solvabilité. Le code de chaque contexte parle son propre dialecte, et c'est très bien ainsi.

Il y a même une conséquence pratique forte : les noms des contextes entrent eux-mêmes dans le langage. Au lieu de dire « le truc de l'équipe de George change, donc on va devoir changer notre truc qui lui parle », on dit « le modèle du Transport change, donc on va devoir adapter le traducteur du contexte Réservation ». La frontière devient un objet de conversation à part entière.

Exemple fil rouge : le contexte de réservation

Evans déroule l'exemple d'une compagnie maritime qui développe une application de réservation de fret (booking). Délimiter le contexte revient à regarder le projet tel qu'il est, pas tel qu'il devrait être idéalement.

  • L'application de réservation consomme le modèle pour afficher et manipuler les objets : elle est dans le contexte.
  • Le schéma de base de données est piloté par le modèle (mapping objet-relationnel volontairement simple) : dans le contexte.
  • Le vieux système de suivi de cargaison (legacy) suit un autre modèle ; on a décidé d'emblée de s'en écarter. Il est hors du contexte, et la traduction entre les deux relève de l'équipe legacy.
  • Une autre équipe modélise la planification des voyages des navires. Les deux équipes ont démarré ensemble en espérant produire un système unifié, et partagent occasionnellement des objets — sans systématique. Elles ne sont pas dans le même contexte, et l'ignorent. C'est le danger : tant qu'aucun processus n'encadre ce partage, des problèmes surviendront.
// Contexte Réservation : son propre modèle d'itinéraire.
class Itineraire {
	constructor(private readonly etapes: Etape[]) {}

	satisfait(spec: SpecificationItineraire): boolean {
		return this.commenceA(spec.origine)
			&& this.termineA(spec.destination);
	}

	private commenceA(origine: NoeudId): boolean {
		return this.etapes[0]?.origine === origine;
	}

	private termineA(destination: NoeudId): boolean {
		return this.etapes.at(-1)?.destination === destination;
	}
	// ...
}

// Contexte Réseau de transport : un AUTRE modèle, optimisé
// pour le calcul de chemins (graphe, matrices). Même domaine,
// conceptualisation totalement différente.
class CheminReseau {
	constructor(private readonly noeuds: NoeudId[]) {}
	// findPath(...) : algorithmique pure, aucune notion d'« étape ».
}

Le gain le plus concret de cet exercice, souligne Evans, est d'avoir mis en lumière le risque du partage informel entre les deux équipes. Ce diagnostic n'émerge que lorsque tout le monde sait où passent les frontières des contextes. Le franchissement de ces frontières — traduction, cartographie des relations — fera l'objet de patrons dédiés (Context Map, Shared Kernel, Anticorruption Layer…), abordés au chapitre suivant.

Pièges à éviter

Quelques erreurs reviennent constamment :

  • Les frontières implicites ou floues. Tant qu'une frontière n'est pas nommée et partagée, les équipes la franchissent sans le savoir et les contextes « bavent » l'un dans l'autre. Donnez un nom à chaque contexte et faites-le entrer dans le langage.
  • Le rêve du modèle canonique universel. Vouloir une seule classe Client, une seule Commande pour toute l'entreprise mène droit à la grosse boule de boue. L'unité se construit par contexte, pas globalement.
  • Réutiliser du code entre contextes. Partager une classe entre deux Bounded Contexts les recouple en douce. L'intégration doit passer par une traduction explicite, pas par un import direct.
  • Confondre la séparation et la résolution. Mettre deux modèles dans deux modules ne résout rien si l'on n'a pas d'abord reconnu pourquoi ils diffèrent et décidé de leur relation.

Note

Plusieurs architectures modernes prolongent directement cette idée. Un microservice est souvent décrit comme l'équivalent déployable d'un Bounded Context (parfois plus fin : plusieurs petits services restent alors dans le même contexte logique). Les Domain Events comme brique tactique, CQRS et l'Event Sourcing — postérieurs à Evans 2003 — sont autant de styles compatibles que l'on déploie à l'intérieur d'un contexte ou pour relier des contextes.

À retenir

  • Un modèle unique pour toute l'entreprise est un mirage : il coûte plus qu'il ne rapporte et finit en grosse boule de boue. Laissez plusieurs modèles coexister, mais pilotez-les.
  • Le Bounded Context est une frontière sémantique explicite : dedans, un modèle et un langage ubiquitaire unifiés ; dehors, d'autres modèles, d'autres dialectes.
  • Un même mot change de sens d'un contexte à l'autre : Client dans Ventes ≠ Support ≠ Facturation. Donnez à chacun son propre type, pas une classe fourre-tout — c'est le nom du contexte qui cadre.
  • La frontière est concrète : une équipe, un dépôt, un schéma, un dialecte. Une équipe par contexte.
  • L'intégration continue garde le modèle sain à l'intérieur : fusion fréquente, tests automatisés, et martèlement constant du langage ubiquitaire pour détecter tôt les fractures.
  • Bounded Context et Ubiquitous Language sont indissociables : un langage par contexte, et les noms des contextes entrent dans le langage de l'équipe.