DDD stratégique
Bounded contexts, langage omniprésent, cartographie de contextes.
Vérifié en septembre 2026 · .NET 10 · environ 16 min
Le DDD tactique organise l'intérieur d'un modèle. Le DDD stratégique décide combien de modèles un système contient, où passe la frontière de chacun et comment ils se parlent. Il sert dès qu'un système dépasse ce qu'une équipe tient en tête : plusieurs métiers qui emploient les mêmes mots dans des sens différents, plusieurs équipes, des systèmes externes qu'on ne contrôle pas. Eric Evans résume le DDD tout entier en trois points : se concentrer sur le domaine cœur, explorer les modèles dans une collaboration créative entre praticiens du métier et du logiciel, parler un langage omniprésent dans un contexte explicitement délimité. Les définitions citées viennent de sa « Domain-Driven Design Reference » de 2015, qui reprend les résumés des motifs de son livre, paru en août 2003 avec un copyright de 2004, et marque d'un astérisque les termes apparus depuis. Le fil rouge reprend la prise de commande du cours sur le DDD tactique, placée dans une boutique qui vend du matériel informatique aux entreprises ; sa règle d'encours est propre à ce cours. Les exemples sont des projets .NET 10 créés par dotnet new, usings implicites compris ; ceux qui s'exécutent affichent ce que disent leurs commentaires.
Le langage omniprésent
La référence le définit comme un langage structuré autour du modèle du domaine, employé par tous les membres de l'équipe à l'intérieur d'un bounded context pour relier toutes leurs activités au logiciel. Le point de départ est un constat : les experts ont leur jargon, les développeurs le leur, le code un troisième, et chaque conversation passe par une traduction qui émousse la communication. Le remède : faire du modèle l'épine dorsale d'un langage, et l'employer sans relâche dans les échanges, les schémas, l'écrit, surtout à l'oral, et dans le code.
// ---- Ventes/Program.cs
var encours = new EncoursClient(plafond: 5000m);
var premiere = new Commande(total: 4200m);
premiere.Valider(encours);
Console.WriteLine(premiere.Etat); // Validee
var seconde = new Commande(total: 1500m);
seconde.Valider(encours);
Console.WriteLine(seconde.Etat); // EnAttenteAccordCredit
public enum EtatCommande
{
Brouillon,
Validee,
EnAttenteAccordCredit,
}
// « L'encours, c'est ce que le client nous doit deja, commandes validees
// comprises. Il ne depasse pas le plafond accorde par le credit client. »
public sealed class EncoursClient(decimal plafond)
{
public decimal Plafond { get; } = plafond;
public decimal Montant { get; private set; }
public bool PeutReserver(decimal montant) => Montant + montant <= Plafond;
public void Reserver(decimal montant) => Montant += montant;
}
// « Quand le commercial valide une commande, on reserve son montant sur
// l'encours du client. Si cela depasse le plafond, la commande attend
// l'accord du credit client. »
public sealed class Commande(decimal total)
{
public decimal Total { get; } = total;
public EtatCommande Etat { get; private set; } = EtatCommande.Brouillon;
public void Valider(EncoursClient encours)
{
if (encours.PeutReserver(Total))
{
encours.Reserver(Total);
Etat = EtatCommande.Validee;
}
else
{
Etat = EtatCommande.EnAttenteAccordCredit;
}
}
}
// Le meme comportement en jargon technique, aussi correct, et muet :
// if (customer.Balance + order.Amount <= customer.Limit)
// {
// customer.Balance += order.Amount;
// order.Status = 2;
// }
// else
// {
// order.Status = 5;
// }Le code se lit comme la phrase de l'expert : valider, réserver, encours, plafond, accord du crédit client. La version en jargon, un statut numérique et un solde incrémenté, ferait la même chose et obligerait chaque lecteur à retraduire. Evans va plus loin que le nommage : un changement dans le langage est un changement du modèle. Le jour où les experts disent « engagement » au lieu d'« encours », parce qu'ils y comptent désormais les devis signés, ce n'est pas un synonyme : la classe se renomme et sa règle change.
Le même mot, deux sens
La référence définit aussi le contexte : le cadre dans lequel un mot apparaît, et qui en fixe le sens. Dans la boutique, « client » désigne pour les ventes une entreprise qui a un plafond d'encours, pour l'expédition un lieu où livrer, pour la facturation un tiers à qui envoyer des factures. « Livré » veut dire, pour le transporteur, que son travail est fini, colis déposé en point relais compris ; pour le service après-vente, que le client a le colis en main. Aucun de ces sens n'est faux, et ils ne tiennent pas dans un seul modèle.
Bounded context : la frontière d'un modèle
Evans part d'un fait : un grand projet a toujours plusieurs modèles, parce que les sous-systèmes servent des utilisateurs aux métiers différents, que des équipes indépendantes résolvent le même problème autrement, que les outils diffèrent. Ces modèles sont inévitables ; ce qui rend le logiciel bogué et peu fiable est de combiner du code bâti sur des modèles distincts. Le bounded context est la description d'une frontière, d'ordinaire un sous-système ou le travail d'une équipe, à l'intérieur de laquelle un modèle donné est défini et applicable. Evans demande de la tracer explicitement, en termes d'organisation des équipes, d'usage dans l'application et de manifestations physiques, bases de code et schémas de base de données, et d'y tenir le modèle strictement cohérent par l'intégration continue. Le modèle unique qui sert tout le monde est la tentation inverse. Ce qui cloche ici est qu'un même champ y porte deux sens :
// ---- Boutique/Program.cs
var client = new Client
{
Id = Guid.NewGuid(),
RaisonSociale = "Atelier Martin",
PlafondEncours = 5000m,
Adresse = "12 rue des Forges, 69007 Lyon",
};
// L'expedition : le client demande que ses colis arrivent sur son chantier.
client.Adresse = "Chantier ZAC Nord, 69100 Villeurbanne";
// La facturation, a la fin du mois.
Console.WriteLine($"Facture envoyee a : {client.Adresse}");
// Facture envoyee a : Chantier ZAC Nord, 69100 Villeurbanne
// Un seul Client pour toute la boutique : chaque service y a ajoute ce dont
// il avait besoin, et Adresse sert a deux services qui n'y mettent pas le
// meme sens.
public sealed class Client
{
public Guid Id { get; init; }
public string RaisonSociale { get; set; } = "";
// Ventes
public decimal PlafondEncours { get; set; }
// Facturation : ou envoyer la facture. Expedition : ou livrer.
public string Adresse { get; set; } = "";
// Expedition
public string? PointRelais { get; set; }
}L'expédition a fait son travail, et la facture du mois part sur un chantier. Aucune ligne n'est fausse prise seule : c'est le modèle qui l'est, parce qu'« adresse » y désigne deux choses. Un modèle par contexte :
// ---- Boutique/Program.cs
using Expedition = Boutique.Expedition;
using Facturation = Boutique.Facturation;
using Ventes = Boutique.Ventes;
var id = Guid.NewGuid();
var client = new Ventes.Client(id, "Atelier Martin", PlafondEncours: 5000m);
var destinataire = new Expedition.Destinataire(id, "12 rue des Forges, 69007 Lyon");
var tiers = new Facturation.Client(id, "Atelier Martin", "12 rue des Forges, 69007 Lyon");
destinataire.LivrerA("Chantier ZAC Nord, 69100 Villeurbanne");
Console.WriteLine($"Colis livres a : {destinataire.AdresseLivraison}");
// Colis livres a : Chantier ZAC Nord, 69100 Villeurbanne
Console.WriteLine($"Facture envoyee a : {tiers.AdresseFacturation}");
// Facture envoyee a : 12 rue des Forges, 69007 Lyon
Console.WriteLine(client.Id == destinataire.ClientId && client.Id == tiers.Id); // True
// Trois contextes, trois modeles. Dans une vraie solution, trois projets ;
// ici, trois espaces de noms pour tenir dans un fichier.
namespace Boutique.Ventes
{
// Pour les ventes, un client est une entreprise a qui l'on accorde un plafond.
public sealed record Client(Guid Id, string RaisonSociale, decimal PlafondEncours);
}
namespace Boutique.Expedition
{
// Pour l'expedition, le client est un destinataire : un lieu ou livrer.
public sealed class Destinataire(Guid clientId, string adresseLivraison)
{
public Guid ClientId { get; } = clientId;
public string AdresseLivraison { get; private set; } = adresseLivraison;
public void LivrerA(string adresse) => AdresseLivraison = adresse;
}
}
namespace Boutique.Facturation
{
// Pour la facturation, un client est un tiers a qui l'on envoie des factures.
public sealed record Client(Guid Id, string RaisonSociale, string AdresseFacturation);
} Chaque contexte a son type et ses mots : deux classes Client, et un Destinataire, le nom que l'expédition donne au client. Ils partagent une identité, pas un objet. Changer le lieu de livraison ne touche plus la facture, et chaque type ne porte que ce que son contexte emploie.
Trouver les frontières
Alberto Brandolini présente l'event storming dans un billet du 18 novembre 2013 : un atelier pour explorer vite un domaine métier complexe, où ceux qui posent les questions et ceux qui connaissent les réponses collent les événements du domaine sur des notes orange, dans l'ordre du temps. Parmi les objectifs annexes qu'il cite figure l'exploration des bounded contexts : des interprétations différentes d'un même terme surgissent dans la discussion, et c'est le moment de tracer les frontières entre les modèles cohérents qui coexisteront. Il signale lui-même que le format a beaucoup évolué depuis.
Sous-domaines : cœur, support, générique
Un sous-domaine découpe le problème, un bounded context découpe la solution. Evans oppose le domaine cœur aux sous-domaines génériques. Le cœur est l'essence du modèle, l'actif réel de l'entreprise, qu'une masse de composants nécessaires finit par masquer : le garder petit, y affecter les meilleurs, et justifier tout investissement ailleurs par ce qu'il apporte au cœur. Les sous-domaines génériques sont des sous-domaines cohérents qui ne sont pas la raison d'être du projet : les isoler dans des modules à part, leur donner une priorité moindre, en écarter les développeurs du cœur, et envisager une solution du marché ou un modèle publié. « Supporting » n'est dans la référence qu'un adjectif, distinct de « generic » : elle sépare du cœur « all generic or supporting elements ». La liste à trois types, sous-domaine de support compris, se trouve chez Vaughn Vernon, dans « Implementing Domain-Driven Design » (2013) ; le guide d'architecture Azure de Microsoft décrit ce dernier comme ce qui fait tourner l'entreprise sans la distinguer de ses concurrents, tout en demandant un développement propre.
<!-- Boutique.slnx : un executable, quatre contextes -->
<Solution>
<Folder Name="/Coeur/">
<!-- La prise de commande entre entreprises, et son plafond d'encours. -->
<Project Path="src/Ventes/Ventes.csproj" />
</Folder>
<Folder Name="/Support/">
<!-- Propres a la boutique, necessaires, sans avantage sur ses concurrents. -->
<Project Path="src/Retours/Retours.csproj" />
<Project Path="src/Expedition/Expedition.csproj" />
</Folder>
<Folder Name="/Generique/">
<!-- La facturation vit dans un progiciel : ce projet lui parle, rien de plus. -->
<Project Path="src/Facturation.Integration/Facturation.Integration.csproj" />
</Folder>
<!-- Le noyau que Ventes et Retours partagent, et l'hote qui assemble le tout. -->
<Project Path="src/Boutique.Noyau/Boutique.Noyau.csproj" />
<Project Path="src/Boutique.Hote/Boutique.Hote.csproj" />
</Solution>Le cœur de la boutique est la prise de commande entre entreprises, avec son plafond d'encours : là, une règle mal tenue coûte de l'argent, et là va le modèle riche du cours sur le DDD tactique. Les retours et l'expédition sont nécessaires et ne distinguent la boutique de personne : un modèle simple suffit. La facturation est confiée à un progiciel, dont le projet ne fait que parler la langue. Ranger la solution ainsi s'inspire d'une consigne d'Evans, le Highlighted Core : signaler les éléments du cœur dans le dépôt, pour que chacun sache sans effort ce qui en fait partie.
Cette solution ne produit qu'un exécutable et contient quatre contextes. La définition d'Evans ne parle pas de déploiement, et le guide .NET de Microsoft sur les microservices le relève : le motif ne dit pas si un bounded context est un service distribué ou une simple frontière logique dans une application déployée d'un bloc. Le guide Azure donne la règle pour qui découpe en services : un microservice n'est pas plus petit qu'un agrégat ni plus grand qu'un bounded context. Découper en services ne crée donc pas de contextes : deux services qui partagent tout leur modèle forment un seul contexte, déployé en deux morceaux.
La carte de contextes
La carte de contextes identifie chaque modèle en jeu, y compris ceux, implicites, des sous-systèmes qui ne sont pas objet, nomme chaque contexte et fait entrer ces noms dans le langage, puis décrit les points de contact : traductions, partages, isolements, niveaux d'influence. Evans demande de cartographier le terrain tel qu'il est et de remettre les transformations à plus tard. Deux relations structurent la carte. En amont-aval, les actions de l'amont affectent la réussite de l'aval, celles de l'aval guère l'amont, comme la pollution d'une ville en amont d'un fleuve ; l'amont peut réussir quel que soit le sort de l'aval. En dépendance mutuelle, deux projets doivent être livrés tous les deux pour que l'un ou l'autre soit une réussite.
| Motif | Ce qu'Evans prescrit | Dans la boutique |
|---|---|---|
| Partenariat (Partnership) * | planification coordonnée et intégration gérée en commun, quand les deux réussissent ou échouent ensemble | Ventes et Expédition, pour lancer la livraison le jour même |
| Noyau partagé (Shared Kernel) | un petit sous-ensemble du modèle et de son code, explicitement délimité, qui ne change pas sans consulter l'autre équipe | Ventes et Retours : identités et montant |
| Client-fournisseur (Customer/Supplier) | les priorités de l'aval entrent dans la planification de l'amont ; tests d'acceptation communs dans l'intégration continue de l'amont | Ventes, fournisseur de l'intégration de la facturation |
| Conformiste (Conformist) | l'aval adhère servilement au modèle d'un amont qui n'a aucune raison de le servir | l'intégration de la facturation reprend tel quel le modèle de facture du progiciel |
| Couche anticorruption (Anticorruption Layer) | l'aval s'isole derrière une couche qui traduit, dans un sens ou dans les deux | Retours face au transporteur |
| Service ouvert (Open-host Service) | un protocole ouvert à tous ceux qui s'intègrent | l'API v1 de Ventes |
| Langue publiée (Published Language) | une langue d'échange partagée et bien documentée | le contrat CommandeV1 |
| Chemins séparés (Separate Ways) | aucune connexion avec les autres contextes | la réservation des salles de réunion |
| Big Ball of Mud * | tracer une frontière autour du désordre, ne pas y modéliser finement, surveiller qu'il ne déborde pas | l'ancien logiciel de gestion, en attendant son remplacement |
Les astérisques sont ceux de la référence : le partenariat et la big ball of mud, reprise de Foote et Yoder, sont entrés dans le langage de motifs après le livre, qui présentait les sept autres.
Partenariat et noyau partagé
Le partenariat répond à la dépendance mutuelle : planifier ensemble, gérer l'intégration en commun, livrer dans la même version les fonctionnalités qui dépendent l'une de l'autre. Le noyau partagé va plus loin et met en commun du modèle et du code, une interdépendance très intime selon Evans, qui peut démultiplier le travail de conception ou le saper. D'où les conditions du tableau, que le code rappelle :
// ---- Boutique.Noyau/Noyau.cs : reference par Ventes et par Retours, et par eux seuls
namespace Boutique.Noyau;
// Le noyau partage : ce que les deux equipes ont convenu d'avoir en commun,
// petit et explicitement delimite. Aucune equipe ne le change sans consulter
// l'autre, et chaque modification fait tourner les tests des deux contextes.
public readonly record struct ClientId(Guid Valeur);
public readonly record struct CommandeId(Guid Valeur);
// Le Montant du cours sur le DDD tactique, reduit a l'essentiel.
public sealed record Montant
{
public Montant(decimal valeur, string devise)
{
ArgumentOutOfRangeException.ThrowIfNegative(valeur);
if (devise is not { Length: 3 } || !devise.All(char.IsAsciiLetterUpper))
{
throw new ArgumentException("Code ISO 4217 attendu, par exemple EUR.", nameof(devise));
}
Valeur = valeur;
Devise = devise;
}
public decimal Valeur { get; }
public string Devise { get; }
}Ventes et Retours partagent l'identité d'un client, celle d'une commande, et le montant qu'un retour rembourse, et rien de plus.
Client-fournisseur, ou conformiste
Quand l'amont peut réussir sans l'aval, l'aval risque d'être à la merci de ses priorités, et l'amont d'être freiné par la peur de le casser. La relation client-fournisseur fait entrer les priorités de l'aval dans la planification de l'amont : ses besoins se négocient et se budgètent. Des tests d'acceptation automatisés, écrits ensemble, valident l'interface attendue ; ajoutés à la suite de l'amont et joués par son intégration continue, ils lui permettent de changer sans craindre d'effet de bord chez l'aval. Ventes est le fournisseur de l'équipe qui intègre la facturation :
// ---- Ventes.Contrats/CommandeV1.cs
namespace Ventes.Contrats;
/// <summary>Une commande, telle que Ventes la publie. La V1 ne change plus.</summary>
/// <param name="Numero">Numero lisible, par exemple C-2026-0042.</param>
/// <param name="Client">Identifiant du client, le meme dans tous les contextes.</param>
/// <param name="Etat">brouillon ou validee.</param>
/// <param name="TotalHt">Total hors taxes.</param>
/// <param name="Devise">Code ISO 4217.</param>
/// <param name="ValideeLe">Date de validation, absente tant que la commande est un brouillon.</param>
public sealed record CommandeV1(
string Numero,
Guid Client,
string Etat,
decimal TotalHt,
string Devise,
DateOnly? ValideeLe);
// ---- Ventes.Contrats.Tests/AttentesDeLaFacturation.cs : projet dotnet new xunit, reference Ventes.Contrats
using System.Text.Json;
using Ventes.Contrats;
namespace Ventes.Contrats.Tests;
// Ecrit avec l'equipe qui integre la facturation, range dans la suite de
// Ventes et joue par son integration continue : un champ du contrat renomme
// ou supprime echoue ici, chez le fournisseur. Ce test ne couvre que le
// contrat : ce que VersV1 y met et la configuration JSON de l'hote demandent
// un test de l'endpoint.
public class AttentesDeLaFacturation
{
[Fact]
public void Une_commande_validee_porte_ce_que_la_facturation_lit()
{
var commande = new CommandeV1(
"C-2026-0042",
Guid.Parse("3f2b8c1e-5d4a-4e6f-9a7b-2c1d0e9f8a76"),
"validee",
1250.00m,
"EUR",
new DateOnly(2026, 9, 26));
// Les options Web sont celles qu'ASP.NET Core emploie par defaut pour ses reponses.
var json = JsonSerializer.SerializeToElement(commande, JsonSerializerOptions.Web);
Assert.Equal("C-2026-0042", json.GetProperty("numero").GetString());
Assert.Equal("validee", json.GetProperty("etat").GetString());
Assert.Equal(1250.00m, json.GetProperty("totalHt").GetDecimal());
Assert.Equal("EUR", json.GetProperty("devise").GetString());
Assert.Equal("2026-09-26", json.GetProperty("valideeLe").GetString());
}
} Renommer TotalHt en TotalHorsTaxes compile toujours, et fait échouer ce test dans l'intégration continue de Ventes : GetProperty("totalHt") lève KeyNotFoundException, avant que la facturation reçoive une seule commande sans total.
Quand l'amont n'a aucune raison de servir l'aval, il ne reste que des choix de l'aval. Un transporteur n'adaptera pas son API aux besoins d'une boutique. La conformité supprime la traduction en adhérant servilement au modèle de l'amont : elle bride l'aval et ne donne sans doute pas le modèle idéal, mais simplifie énormément l'intégration et fournit un langage commun avec l'amont. Elle vaut quand le modèle de l'amont convient. Quand il ne convient pas, il reste la couche anticorruption.
La couche anticorruption
Evans la prescrit quand le contrôle ou la communication ne suffisent pas à un noyau partagé, un partenariat ou une relation client-fournisseur : une large interface avec un système amont finit par submerger l'intention du modèle aval, qu'on retouche au coup par coup pour qu'il ressemble à l'autre. Le service après-vente a une règle : un produit se retourne dans les quatorze jours qui suivent sa livraison. Ce qui cloche dans cette version est que son domaine adopte le modèle du transporteur, et avec lui son mot « livré » :
// ---- Transporteur.Api/SuiviColis.cs : le paquet du transporteur, tel qu'il le livre
namespace Transporteur.Api;
// Statut : PEC pris en charge ; LIV livre, a l'adresse ou depose en point
// relais ; RET retire en point relais. DateEvenement : aaaammjj.
public sealed record SuiviColis(
string NumeroColis,
string Statut,
string DateEvenement,
string? CodePointRelais);
public interface IClientSuivi
{
SuiviColis Suivre(string numeroColis);
}
// ---- Retours.Domaine/PolitiqueRetour.cs : reference Transporteur.Api
using System.Globalization;
using Transporteur.Api;
namespace Retours.Domaine;
public static class PolitiqueRetour
{
// « Un produit se retourne dans les quatorze jours qui suivent sa livraison. »
// Le transporteur dit LIV pour livre : la regle reprend son mot.
public static bool RetourPossible(SuiviColis suivi, DateOnly aujourdhui) =>
suivi.Statut == "LIV"
&& aujourdhui
<= DateOnly
.ParseExact(suivi.DateEvenement, "yyyyMMdd", CultureInfo.InvariantCulture)
.AddDays(14);
}
// ---- Retours.Console/Program.cs : reference Retours.Domaine
using Retours.Domaine;
using Transporteur.Api;
var aujourdhui = new DateOnly(2026, 9, 26);
var transporteur = new TransporteurSimule();
// Retire par le client au point relais il y a trois jours.
Console.WriteLine(PolitiqueRetour.RetourPossible(transporteur.Suivre("CP-1001"), aujourdhui)); // False
// Depose au point relais avant-hier, pas encore retire.
Console.WriteLine(PolitiqueRetour.RetourPossible(transporteur.Suivre("CP-1002"), aujourdhui)); // True
// Ce que le transporteur repond pour ces deux colis.
sealed class TransporteurSimule : IClientSuivi
{
public SuiviColis Suivre(string numeroColis) =>
numeroColis switch
{
"CP-1001" => new SuiviColis("CP-1001", "RET", "20260923", "RELAIS-042"),
"CP-1002" => new SuiviColis("CP-1002", "LIV", "20260924", "RELAIS-042"),
_ => throw new ArgumentException("Colis inconnu.", nameof(numeroColis)),
};
}Le colis retiré au point relais il y a trois jours n'est pas « LIV » mais « RET », et son retour est refusé ; celui qui attend au point relais est « LIV », et le client pourrait retourner un produit qu'il n'a jamais reçu. Le transporteur n'a rien de faux : pour lui, un colis déposé en point relais est livré. C'est le domaine qui a pris son mot sans son sens, et chaque endroit du code qui lit un statut doit refaire la même interprétation. La couche anticorruption lui donne l'information dans ses propres termes :
// ---- Retours.Domaine/PolitiqueRetour.cs : ne reference plus rien
namespace Retours.Domaine;
// La langue du service apres-vente : le client a recu son colis, tel jour.
public sealed record Reception(DateOnly Le);
// Le port, declare par le domaine, dans ses mots.
public interface IReceptions
{
Reception? DeColis(string numeroColis);
}
public static class PolitiqueRetour
{
public const int DelaiEnJours = 14;
// « Un produit se retourne dans les quatorze jours qui suivent sa livraison. »
public static bool RetourPossible(Reception? reception, DateOnly aujourdhui) =>
reception is not null && aujourdhui <= reception.Le.AddDays(DelaiEnJours);
}
// ---- Retours.Infrastructure/ReceptionsTransporteur.cs : reference Retours.Domaine et Transporteur.Api
using System.Globalization;
using Retours.Domaine;
using Transporteur.Api;
namespace Retours.Infrastructure;
// La couche anticorruption : la seule classe qui connait les deux modeles.
// Elle parle au transporteur par son interface telle qu'elle est, et traduit.
public sealed class ReceptionsTransporteur(IClientSuivi transporteur) : IReceptions
{
public Reception? DeColis(string numeroColis) => Traduire(transporteur.Suivre(numeroColis));
private static Reception? Traduire(SuiviColis suivi) =>
(suivi.Statut, suivi.CodePointRelais) switch
{
("PEC", _) => null,
// Livre a l'adresse : le client a le colis en main.
("LIV", null) => new Reception(Date(suivi)),
// Depose en point relais : livre pour le transporteur, pas pour le client.
("LIV", _) => null,
("RET", _) => new Reception(Date(suivi)),
_ => throw new InvalidOperationException(
$"Statut du transporteur inconnu : {suivi.Statut}."),
};
private static DateOnly Date(SuiviColis suivi) =>
DateOnly.ParseExact(suivi.DateEvenement, "yyyyMMdd", CultureInfo.InvariantCulture);
}
// ---- Retours.Console/Program.cs : reference Retours.Infrastructure
// Transporteur.Api : inchange, celui de la version precedente.
using Retours.Domaine;
using Retours.Infrastructure;
var aujourdhui = new DateOnly(2026, 9, 26);
IReceptions receptions = new ReceptionsTransporteur(new TransporteurSimule());
Console.WriteLine(PolitiqueRetour.RetourPossible(receptions.DeColis("CP-1001"), aujourdhui)); // True
Console.WriteLine(PolitiqueRetour.RetourPossible(receptions.DeColis("CP-1002"), aujourdhui)); // False
// ---- Retours.Console/TransporteurSimule.cs : la classe de la version precedente, dans son fichier
using Transporteur.Api;
sealed class TransporteurSimule : IClientSuivi
{
public SuiviColis Suivre(string numeroColis) =>
numeroColis switch
{
"CP-1001" => new SuiviColis("CP-1001", "RET", "20260923", "RELAIS-042"),
"CP-1002" => new SuiviColis("CP-1002", "LIV", "20260924", "RELAIS-042"),
_ => throw new ArgumentException("Colis inconnu.", nameof(numeroColis)),
};
} Le domaine ne référence plus le paquet du transporteur. Il parle de réception, son mot à lui, et sa règle se lit sans connaître un seul code de statut. C'est la forme d'un adaptateur secondaire du cours
Service ouvert, langue publiée, chemins séparés
Quand un sous-système est très demandé, adapter un traducteur à chaque client enlise l'équipe. Le service ouvert définit un protocole qui donne accès au sous-système comme un ensemble de services, ouvert à tous ceux qui doivent s'y intégrer, et enrichi pour les besoins nouveaux ; le besoin idiosyncratique d'une seule équipe est servi par un traducteur ponctuel, pour que le protocole commun reste simple et cohérent. Le fournisseur est en amont, chaque client en aval, d'ordinaire les uns conformistes, les autres derrière une couche anticorruption. La langue publiée règle le format : traduire directement vers les modèles de chacun peut ne pas être une bonne solution, et un modèle de domaine employé comme langue d'échange se fige. Il faut une langue partagée et bien documentée, souvent combinée au service ouvert, comme les normes d'échange de données que se donnent des filières entières.
// ---- Ventes.Api/Program.cs : projet dotnet new web, reference Ventes.Contrats
using Ventes.Contrats;
var app = WebApplication.CreateBuilder(args).Build();
// Le service ouvert de Ventes : un protocole, le meme pour tous les
// contextes qui s'integrent, et sa version dans l'URL.
app.MapGet(
"/v1/commandes/{numero}",
(string numero) =>
Commandes.Trouver(numero) is { } commande
? Results.Ok(commande.VersV1())
: Results.NotFound());
app.Run();
// Le modele interne, qui ne sort pas du contexte.
sealed record Ligne(string Reference, int Quantite, decimal PrixUnitaire);
sealed class Commande(string numero, Guid client, IReadOnlyList<Ligne> lignes)
{
public string Numero { get; } = numero;
public Guid Client { get; } = client;
public IReadOnlyList<Ligne> Lignes { get; } = lignes;
public DateOnly? ValideeLe { get; private set; }
public void Valider(DateOnly le) => ValideeLe = le;
// La traduction vers la langue publiee : le seul endroit qui connait les deux.
public CommandeV1 VersV1() =>
new(
Numero,
Client,
ValideeLe is null ? "brouillon" : "validee",
Lignes.Sum(l => l.Quantite * l.PrixUnitaire),
"EUR",
ValideeLe);
}
static class Commandes
{
private static readonly Commande Exemple = Creer();
public static Commande? Trouver(string numero) => numero == Exemple.Numero ? Exemple : null;
private static Commande Creer()
{
var commande = new Commande(
"C-2026-0042",
Guid.Parse("3f2b8c1e-5d4a-4e6f-9a7b-2c1d0e9f8a76"),
[new("ECRAN-27", 3, 300.00m), new("CABLE-HDMI", 14, 25.00m)]);
commande.Valider(new DateOnly(2026, 9, 26));
return commande;
}
}# Ventes.Api (ASP.NET Core 10), lance avec --urls http://localhost:5651
curl -s http://localhost:5651/v1/commandes/C-2026-0042
{"numero":"C-2026-0042","client":"3f2b8c1e-5d4a-4e6f-9a7b-2c1d0e9f8a76","etat":"validee","totalHt":1250.00,"devise":"EUR","valideeLe":"2026-09-26"}
# Un numero inconnu : 404, sans corps.
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5651/v1/commandes/C-2026-9999
404CommandeV1 est la langue ; la Commande interne, avec ses lignes, ne sort pas du contexte, et Ventes peut la remanier sans prévenir personne tant que VersV1 tient.
L'intégration coûte toujours, et son bénéfice est parfois faible. Evans demande d'être impitoyable sur les exigences : quand deux ensembles de fonctionnalités n'ont pas de relation significative, déclarer un contexte sans aucune connexion aux autres, où trouver des solutions simples et spécialisées. La réservation des salles de réunion de la boutique n'a rien à dire aux commandes ; la relier aux clients parce que c'est possible ajouterait une intégration à maintenir, pour rien.