La cartographie des contextes
Comment les contextes communiquent : Anti-Corruption Layer, Shared Kernel, Customer/Supplier, Open Host Service…
Sur un grand système, plusieurs modèles coexistent. C'est inévitable, et le nier mène droit au désastre. Evans ouvre son chapitre sur une anecdote édifiante : deux équipes travaillent en parallèle sur un même système. L'une construit une classe Charge pour la facturation client ; l'autre, ayant besoin d'un concept de « charge » pour le paiement fournisseur, décide de réutiliser la classe existante. Elle ajoute un attribut par-ci, renomme un champ par-là, et son module compile. Quelques jours plus tard, des « charges » fantômes apparaissent, les rapports fiscaux plantent. Les deux équipes avaient le même mot pour deux concepts différents — un faux ami — et personne ne l'avait vu.
La leçon n'est pas « il ne faut jamais partager de code ». C'est qu'on ne peut décider de partager ou non que si l'on connaît d'abord les frontières. Une fois les contextes délimités (Bounded Contexts) posés — c'était l'objet du chapitre précédent —, il faut une vue d'ensemble qui les relie : la carte de contexte (Context Map). Et il faut nommer la nature de chaque relation, car toutes ne se valent pas. Ce chapitre déroule les patrons de relation entre contextes, leurs forces et leurs coûts, en montrant que la question est autant organisationnelle que technique.
La carte de contexte : une vue d'ensemble honnête
Un contexte délimité isolé laisse subsister un angle mort : que se passe-t-il à ses frontières ? Les autres équipes ne connaissent pas vos limites et, sans le savoir, brouillent les bords. Quand deux contextes doivent communiquer, ils ont tendance à déteindre l'un sur l'autre. La carte de contexte est la réponse : on identifie chaque modèle en jeu sur le projet, on lui donne un nom — ce nom entre dans le langage omniprésent (Ubiquitous Language) de l'équipe — et on décrit les points de contact entre les modèles.
Le point capital, répété par Evans, est que la carte décrit la réalité, pas l'idéal. On cartographie le terrain tel qu'il est. Si une relation est confuse, on la décrit comme confuse ; si un contexte est un marécage incohérent, on dessine un dragon dessus et on continue. La tentation inverse — dessiner l'architecture rêvée — produit une carte qui ment, donc inutile.
À retenir
La carte de contexte vit dans le chevauchement entre la gestion de projet et la conception logicielle. Les frontières de modèle suivent naturellement les contours des équipes : des gens qui travaillent ensemble partagent un contexte ; des gens qui ne se parlent pas divergent, même au sein d'une même équipe. C'est la loi de Conway en action.
La carte n'a pas de forme imposée. Un diagramme informel, une description textuelle, parfois une simple conversation suffisent. Deux exigences seulement : chaque contexte doit avoir un nom entré dans le langage commun, et chacun doit savoir où passent les frontières. Le bénéfice se mesure dans la façon de parler. On ne dit plus « le truc de l'équipe de Georges change, donc on va devoir changer notre truc qui lui parle », mais « le modèle Réseau de transport change, donc on va devoir adapter le traducteur du contexte Réservation ».
Reconnaître les fractures : faux amis et concepts dupliqués
Avant de relier des contextes, encore faut-il savoir quand un modèle se fissure. Evans distingue deux symptômes lorsqu'on combine des éléments de modèles distincts.
| Fracture | Description | Conséquence |
|---|---|---|
| Concepts dupliqués | Deux éléments du modèle représentent en réalité le même concept. | Toute évolution doit être répercutée à deux endroits, avec conversions ; en pratique, les deux divergent. |
| Faux amis (false cognates) | Deux personnes emploient le même terme en croyant parler de la même chose, alors que non. | Le plus insidieux : équipes qui se marchent sur le code, données contradictoires, communication faussée. |
L'histoire des deux Charge est un faux ami pur. Comme le résume Evans avec humour, un anglophone apprenant l'espagnol qui croit que embarazada signifie « embarrassé » se trompe : cela veut dire « enceinte ». Oups. Le premier signal d'alarme d'une fracture est presque toujours une confusion de langage, bien avant que le code ne plante.
Le spectre des relations entre contextes
Les patrons qui suivent forment un spectre de coopération, du plus collaboratif au plus défensif. Deux variables dominent le choix : le degré de contrôle que vous avez sur l'autre modèle, et le niveau de coopération entre équipes.
| Patron | Contrôle / coopération | En une phrase |
|---|---|---|
| Shared Kernel | Deux équipes alliées | Partager un petit noyau de modèle commun. |
| Customer/Supplier | Amont/aval, même direction | L'aval est un client prioritaire de l'amont. |
| Conformist | Amont indifférent | L'aval épouse le modèle amont, faute de pouvoir. |
| Anticorruption Layer | Aucun contrôle, modèle hostile | Traduire et s'isoler derrière une couche défensive. |
| Open Host Service | Vous êtes l'amont, demandé par beaucoup | Publier un protocole ouvert de services. |
| Separate Ways | Aucun besoin d'intégration | Ne pas intégrer du tout. |
Note
Toute intégration a un coût. Du noyau partagé à la couche anticorruption, en passant par la conformité, chaque relation impose une charge de coordination ou de traduction. Avant de choisir un patron, demandez-vous si l'intégration est vraiment nécessaire. Parfois la meilleure réponse est Separate Ways.
Shared Kernel : un noyau de modèle partagé
Quand deux équipes travaillent sur des applications étroitement liées, synchroniser tout leur modèle (l'intégration continue d'un contexte unique) peut coûter trop cher. À l'inverse, les laisser foncer chacune de leur côté produit des pièces qui ne s'emboîtent pas, et l'on finit par dépenser en couches de traduction plus que ce qu'aurait coûté l'unification. Le noyau partagé (Shared Kernel) est le compromis.
On désigne un sous-ensemble du modèle — souvent le cœur de domaine (Core Domain) ou un sous-domaine générique — que les deux équipes acceptent de partager, code et schéma de base inclus. Ce sous-ensemble a un statut spécial : on ne le modifie pas sans consulter l'autre équipe, et toute évolution doit faire passer les tests des deux équipes.
// Noyau partagé : le vocabulaire commun aux deux contextes
// (Réservation et Planification des voyages).
// Modifier ce fichier exige l'accord des deux équipes.
export class CodeLieu {
private constructor(private readonly code: string) {}
static depuis(code: string): CodeLieu {
if (!/^[A-Z]{5}$/.test(code)) {
throw new Error(`Code lieu invalide : ${code}`);
}
return new CodeLieu(code);
}
estEgalA(autre: CodeLieu): boolean {
return this.code === autre.code;
}
toString(): string {
return this.code;
}
} Le but n'est pas d'éliminer toute duplication (ce serait un contexte unique), mais de la réduire assez pour rendre l'intégration aisée. C'est un équilibre délicat : il faut une communication ouverte et un accord constant sur ce qui constitue le noyau. Dans le résumé de Vernon, le partenariat (Partnership), patron voisin — deux équipes qui « réussissent ou échouent ensemble » —, traduit un engagement encore plus fort, à réserver aux relations à fort enjeu et à durée limitée.
Customer/Supplier : amont et aval avec priorités négociées
Très souvent, un sous-système en alimente un autre : l'amont (Supplier) produit des données, l'aval (Customer) les consomme pour de l'analyse ou d'autres fonctions qui ne reviennent pas vers l'amont. Les dépendances ne vont que dans un sens, ce qui simplifie la traduction. Mais une tension politique guette.
Si l'aval a un droit de veto sur les changements, l'amont est bridé, paralysé par la peur de casser le système en aval. À l'inverse — cas le plus fréquent — l'aval devient un parent pauvre qui vient mendier ses besoins à une équipe amont sans obligation envers lui. Le patron Customer/Supplier corrige cela en formalisant la relation.
Astuce
Deux éléments rendent ce patron efficace : (1) la relation est bien celle d'un client prioritaire, dont les besoins sont budgétés et planifiés avec l'amont ; (2) une suite de tests d'acceptation automatisés, écrite en commun et exécutée dans l'intégration continue de l'amont, garantit que l'aval ne sera pas cassé en silence.
Ces tests sont le cœur du dispositif. L'amont les fait tourner à chaque build ; toute modification d'un test implique une négociation, car elle signifie un changement d'interface. Evans file la métaphore du relais : le coureur de devant ne peut pas regarder sans cesse derrière lui, il doit pouvoir faire confiance au passage de témoin.
// Test d'acceptation co-écrit par l'équipe Analyse de rendement
// (Customer) et exécuté chez l'équipe Réservation (Supplier).
// S'il casse, l'interface a changé : on en discute.
test("l'export réservation expose le tonnage et le tarif", () => {
const ligne = exportReservation.pour(reservationId);
expect(ligne).toMatchObject({
reservationId: expect.any(String),
tonnage: expect.any(Number),
tarifEur: expect.any(Number),
});
}); Ce patron ne fonctionne que si les deux équipes partagent in fine des objectifs : même hiérarchie de management, ou véritable relation commerciale client-fournisseur. Sans rien pour motiver l'amont, la situation bascule.
Conformist : se conformer faute de pouvoir
Quand l'amont n'a aucune motivation à servir l'aval — équipes éloignées dans la hiérarchie, fournisseur externe pour qui vous êtes un client négligeable —, le patron Customer/Supplier s'effondre. L'aval est livré à lui-même. L'altruisme amont produit des promesses non tenues ; planifier sur des fonctionnalités qui n'arriveront jamais ne fait que retarder le projet.
Trois chemins s'ouvrent alors. Abandonner la dépendance (Separate Ways). La traduire défensivement (Anticorruption Layer, ci-dessous). Ou, si le modèle amont est de qualité acceptable et de style compatible, renoncer à un modèle propre et épouser servilement celui de l'amont : c'est le conformiste (Conformist).
// Conformist : on adopte tel quel le vocabulaire d'un service
// externe dominant. Pas de traduction, donc pas d'isolation.
// On hérite de ses choix — y compris le snake_case et ses énumérations.
interface CommandeMarketplace {
order_id: string;
fulfillment_status: "pending" | "shipped" | "delivered";
line_items: Array<{ sku: string; qty: number }>;
}
// Notre code parle directement le dialecte du fournisseur.
function estLivree(c: CommandeMarketplace): boolean {
return c.fulfillment_status === "delivered";
} Cette décision approfondit la dépendance : votre application se limite aux capacités du modèle amont, plus des extensions purement additives. C'est émotionnellement déplaisant, ce qui explique qu'on le choisisse moins souvent qu'on ne le devrait. Mais la réduction de complexité est énorme, et l'on partage gratuitement un langage avec l'amont.
Note
Se conformer n'est pas toujours un mal. Quand vous intégrez un composant sur étagère à large interface, vous conformer à son modèle est souvent le bon choix : sinon, à quoi bon ce composant ? S'il est assez bon pour vous apporter de la valeur, c'est qu'il y a de la connaissance condensée dans sa conception. Là où vous le touchez, suivez le leader.
Anticorruption Layer : la couche défensive
C'est le patron le plus important en pratique, parce que tout nouveau système doit s'intégrer à du legacy ou à des systèmes externes dont les modèles sont souvent faibles et toujours différents du vôtre. Quand l'interface avec l'autre système est large, la difficulté de relier les deux modèles peut submerger l'intention de votre nouveau modèle, le déformant peu à peu jusqu'à ce qu'il ressemble à l'ancien. La couche anticorruption (Anticorruption Layer, ou ACL) est le rempart.
L'erreur classique est de croire qu'on échange des « données primitives » au sens neutre. C'est faux : le sens naît de la façon dont les données sont associées dans chaque modèle. Une interface de bas niveau vous prive de la puissance explicative de l'autre modèle tout en vous chargeant d'interpréter des primitives étrangères. L'ACL n'est donc pas un mécanisme de transport ; c'est un mécanisme qui traduit des objets et des actions conceptuels d'un modèle vers l'autre.
Evans en décrit l'anatomie comme une combinaison de trois rôles, qui correspondent à des patrons du Gang of Four :
- Façade : une interface alternative qui simplifie l'accès au sous-système externe, écrite strictement dans le modèle de l'autre système. Elle masque le désordre, sans rien traduire.
- Adaptateur (Adapter) : il expose un service dans votre modèle et sait formuler la requête équivalente vers la façade. C'est lui qui relie les deux protocoles.
- Traducteur (Translator) : objet léger, sans état, qui effectue la conversion conceptuelle proprement dite. Le séparer de l'adaptateur rend les deux bien plus lisibles.
// Notre modèle propre, exprimé dans NOTRE langage omniprésent.
export class Itineraire {
constructor(public readonly trajets: ReadonlyArray<Trajet>) {}
}
export class Trajet {
constructor(
public readonly navireVoyageId: string,
public readonly chargement: Escale,
public readonly dechargement: Escale,
) {}
}
export class Escale {
constructor(
public readonly lieu: CodeLieu,
public readonly date: Date,
) {}
} // La forme imposée par le système de routage externe.
interface ReponseRoutageExterne {
nodes: Array<{
location_code: string;
op_type: "DEP" | "ARR";
vessel_voyage: string;
ts: number; // epoch millis
}>;
} // --- Façade : parle le langage de l'autre système, rien de plus. ---
class FacadeRoutage {
constructor(private readonly api: ClientHttp) {}
async trouverChemin(codes: string[]): Promise<ReponseRoutageExterne> {
return this.api.post("/network/find-path", { locations: codes });
}
}
// --- Traducteur : conversion conceptuelle, sans état. ---
class TraducteurItineraire {
versItineraire(reponse: ReponseRoutageExterne): Itineraire {
const trajets: Trajet[] = [];
for (let i = 0; i < reponse.nodes.length; i += 2) {
const depart = reponse.nodes[i];
const arrivee = reponse.nodes[i + 1];
trajets.push(
new Trajet(
depart.vessel_voyage,
new Escale(CodeLieu.depuis(depart.location_code),
new Date(depart.ts)),
new Escale(CodeLieu.depuis(arrivee.location_code),
new Date(arrivee.ts)),
),
);
}
return new Itineraire(trajets);
}
}
// --- Adaptateur : expose un SERVICE dans NOTRE modèle. ---
export class ServiceRoutage {
constructor(
private readonly facade: FacadeRoutage,
private readonly traducteur: TraducteurItineraire,
) {}
async router(spec: SpecificationItineraire): Promise<Itineraire> {
const codes = spec.lieuxOrdonnes().map((l) => l.toString());
const reponse = await this.facade.trouverChemin(codes);
return this.traducteur.versItineraire(reponse);
}
} Le reste de votre contexte ne voit que ServiceRoutage, qui renvoie un Itineraire de votre modèle. Tout le vocabulaire étranger — op_type, epoch millis, snake_case — reste prisonnier de l'ACL. Si l'API externe change, un seul endroit est touché.
Astuce
Reliez l'ACL au patron Adaptateur : un adaptateur enveloppe un fournisseur pour qu'un client utilise un protocole différent de celui de l'implémentation. La nuance d'Evans : ici, c'est vous qui choisissez l'interface adaptée, et l'objet adapté n'est souvent même pas un objet. L'accent porte sur la traduction entre deux modèles, pas sur la simple conformité à une interface attendue.
L'ACL a un coût réel — Evans le compare à la Grande Muraille de Chine, qui protégea une civilisation mais ruina une dynastie. Quelques garde-fous : la façade est inutile si l'autre système a déjà une interface propre ; l'ACL peut être bidirectionnelle ; et si la traduction devient ingérable, c'est peut-être le signe qu'il faut basculer vers Conformist. Enfin, l'ACL est aussi le bon outil pour relier deux de vos propres contextes fondés sur des modèles différents.
Open Host Service et Published Language : s'ouvrir aux autres
Les patrons précédents supposaient que vous insériez une couche de traduction pour chaque système externe. Cette approche au cas par cas convient à des intégrations ponctuelles. Mais lorsque votre sous-système devient très demandé — beaucoup d'autres veulent s'y intégrer —, écrire un traducteur sur mesure pour chacun engloutit l'équipe. On refait sans cesse la même chose.
Le service hôte ouvert (Open Host Service) inverse alors la charge : au lieu d'attendre que chaque client construise son ACL, vous définissez un protocole donnant accès à votre sous-système comme un ensemble de services, et vous l'ouvrez à tous. Vous l'enrichissez pour couvrir les nouveaux besoins, sauf cas idiosyncrasiques que l'on traite par un traducteur ponctuel afin de garder le protocole simple et cohérent.
// Open Host Service : un contrat stable, documenté, conçu autour
// des cas d'usage des consommateurs (pas calqué sur nos agrégats).
export interface ServiceHoteReservation {
// Ressource synthétique, pensée pour les clients, versionnée.
consulterReservation(id: string): Promise<ReservationV1>;
listerEscales(id: string): Promise<EscaleV1[]>;
} Cette formalisation suppose un vocabulaire de modèle partagé : les autres équipes se couplent à votre dialecte et doivent l'apprendre. On réduit ce couplage en exprimant l'interface dans une langue publiée (Published Language) : un langage d'échange bien documenté, accessible à toute la communauté intéressée, dans lequel chacun traduit en entrée et en sortie. La langue n'a pas à être inventée ; on peut en adopter une existante. L'exemple historique d'Evans est le Chemical Markup Language, dialecte XML pour échanger des formules chimiques entre des programmes aux modèles incompatibles.
À retenir
La distinction clé : un Open Host Service est le protocole de services que vous exposez ; une Published Language est le format d'échange documenté et stable qu'il emploie. Souvent un OHS sert et consomme une Published Language — aujourd'hui un schéma JSON, OpenAPI, Protobuf ou Avro. Comme la langue devient un médium de communication, elle ne peut plus changer librement : elle doit rester stable.
Une mise en garde reprise par Vernon : ne calquez pas vos ressources publiées directement sur vos agrégats internes. Cela forcerait chaque client en relation Conformist, et toute évolution du modèle casserait l'interface. Concevez des ressources synthétiques, dictées par les cas d'usage des clients.
Separate Ways : assumer la non-intégration
Le patron le plus sous-estimé est aussi le plus simple : ne pas intégrer du tout. L'intégration est toujours coûteuse et son bénéfice parfois minime. Si deux ensembles de fonctionnalités n'ont pas de relation significative — ils ne s'appellent pas, ne partagent ni objets ni données pendant leur exécution —, on peut les séparer complètement. Le fait que deux fonctions apparaissent dans un même cas d'usage ne signifie pas qu'elles doivent être intégrées dans le code.
Evans raconte un projet d'assurance paralysé pendant un an par l'ambition de tout intégrer. Une semaine de remise à plat révéla que plusieurs fonctionnalités n'apportaient aucune valeur à être intégrées : les experts avaient besoin d'accéder à de vieilles bases, mais aucune autre partie du système n'utilisait ces données. La solution fut des petites applications indépendantes, reliées au mieux par des liens sur une page intranet ou des boutons sur le bureau. Plusieurs capacités furent livrées « presque du jour au lendemain ».
// Separate Ways : deux contextes qui ne partagent rien.
// L'intégration se limite à un lancement depuis le même menu.
const menu = [
{ libelle: "Réservation", action: () => ouvrir("/reservation") },
{ libelle: "Rapport experts", action: () => ouvrir("/intranet/exp") },
]; Le choix n'est pas sans conséquence : il ferme des portes. Fusionner plus tard des modèles développés en isolation totale est difficile et exigera des couches de traduction. Mais c'est un risque que l'on assume sciemment, plutôt que de payer le prix d'une intégration dont personne ne profite.
Prolongements modernes
La carte de contexte d'Evans (2003) reste d'actualité, mais l'écosystème DDD l'a enrichie de techniques postérieures au livre, qu'il faut savoir distinguer du texte original :
- Messagerie asynchrone et événements de domaine (Domain Events) comme mode d'intégration : un contexte publie un événement (par exemple
PolicyIssued), d'autres y réagissent. Les consommateurs ne dépendent que du schéma de l'événement (sa Published Language), pas des classes de l'émetteur — ce qui les préserve d'une relation Conformist forcée. - Event Storming : atelier collaboratif pour découvrir contextes et frontières, popularisé bien après 2003.
- Big Ball of Mud (terme de Foote et Yoder, repris par Vernon comme anti-patron de cartographie) : un système sans frontières ni structure. La parade reste la même : une couche anticorruption contre chaque marécage legacy, pour ne jamais « parler cette langue ».
Ces ajouts ne contredisent pas Evans : ils outillent la même idée fondatrice — rendre les frontières explicites et traduire consciemment ce qui les traverse.
À retenir
- La carte de contexte décrit la réalité du terrain — contextes, frontières et rapports d'équipes (Conway) —, pas l'architecture idéale ; chaque contexte reçoit un nom entré dans le langage commun.
- Les fractures se trahissent par le langage : faux amis (même mot, sens différents) et concepts dupliqués sont les deux symptômes à traquer.
- Les patrons forment un spectre de coopération : Shared Kernel et Customer/Supplier (alliés), Conformist (subi), Anticorruption Layer (défensif), Open Host Service (vous êtes l'amont demandé), Separate Ways (pas d'intégration).
- L'Anticorruption Layer est le plus crucial en pratique : façade + adaptateur + traducteur isolent votre modèle d'un système externe ou legacy ; tout le vocabulaire étranger reste prisonnier de la couche.
- Open Host Service + Published Language s'imposent quand beaucoup veulent s'intégrer à vous : un protocole ouvert servi par un format d'échange stable et documenté.
- Toute intégration coûte cher : choisissez le patron consciemment, et n'hésitez pas à assumer Separate Ways quand le bénéfice est nul.