14. DDD stratégique#

Le Domain-Driven Design (DDD) est une approche de conception logicielle proposée par Eric Evans en 2003. L’idée centrale : la complexité d’un logiciel métier est avant tout une complexité de domaine, et le code doit refléter fidèlement la réalité métier plutôt que de l’abstraire derrière des couches techniques génériques. Le DDD se divise en deux grandes familles d’outils : les outils stratégiques, qui guident la décomposition du système à grande échelle, et les outils tactiques, qui organisent le code au sein d’un sous-système. Ce chapitre traite du niveau stratégique.

Pourquoi le DDD — complexité du domaine métier#

La plupart des projets logiciels échouent non par manque de compétences techniques, mais parce que les développeurs ne comprennent pas suffisamment le domaine métier qu’ils automatisent. Le code devient un miroir déformant : les experts métier ne reconnaissent pas leur vocabulaire dans le code, et les développeurs ne comprennent plus les règles qu’ils ont implémentées six mois auparavant.

Le modèle anémique#

Martin Fowler a décrit l”antipattern du modèle anémique : des classes qui ne sont que des conteneurs de données (getters/setters) sans aucun comportement. Toute la logique métier se retrouve dans des « services » ou des « managers » qui manipulent ces structures de l’extérieur.

# Modèle anémique — antipattern
class Order:
    def __init__(self):
        self.items = []
        self.status = "pending"
        self.total = 0.0

class OrderService:
    def add_item(self, order, item, quantity):
        # Logique métier éparpillée dans un service externe
        if order.status != "pending":
            raise Exception("Cannot add item")
        order.items.append({"item": item, "qty": quantity})
        order.total += item.price * quantity

Ce modèle souffre de plusieurs défauts : la logique est dispersée dans de multiples services, les invariants ne sont pas garantis par les objets eux-mêmes, et il est impossible de comprendre les règles en lisant une seule classe.

Le modèle riche#

Un modèle riche encapsule la logique métier dans les entités elles-mêmes :

# Modèle riche — approche DDD
class Order:
    def __init__(self, order_id: str, customer_id: str):
        self._id = order_id
        self._customer_id = customer_id
        self._items = []
        self._status = OrderStatus.PENDING
        self._events = []

    def add_item(self, product: "Product", quantity: int) -> None:
        if self._status != OrderStatus.PENDING:
            raise OrderAlreadyConfirmedException(self._id)
        if quantity <= 0:
            raise InvalidQuantityException(quantity)
        self._items.append(OrderItem(product, quantity))
        self._events.append(ItemAddedToOrder(self._id, product.id, quantity))

    def confirm(self) -> None:
        if not self._items:
            raise EmptyOrderException(self._id)
        self._status = OrderStatus.CONFIRMED
        self._events.append(OrderConfirmed(self._id, self.total))

    @property
    def total(self) -> float:
        return sum(item.subtotal for item in self._items)

Quand recourir au DDD#

Le DDD est particulièrement pertinent quand le domaine est complexe et riche en règles métier, que les experts métier sont disponibles et impliqués, et que le projet a une durée de vie longue. Pour un CRUD simple ou un script d’automatisation, le DDD représente une surcharge inutile.

Ubiquitous Language — le langage commun#

L”Ubiquitous Language est un vocabulaire partagé entre développeurs et experts métier, utilisé à la fois dans les conversations, la documentation et le code source. C’est la colle qui unit la compréhension métier et l’implémentation technique.

Construire le langage commun#

Le processus est itératif. Les développeurs participent aux sessions avec les experts métier non pas pour « recueillir des exigences » mais pour comprendre profondément le domaine. Les termes ambigus ou contradictoires sont identifiés et clarifiés.

Exemple dans un système e-commerce :

Terme imprécis

Terme du langage commun

Précision

Produit

Product

Ce qui est vendu dans le catalogue

Article

OrderItem

Une ligne dans une commande

Client

Customer (Sales)

Personne ayant passé une commande

Compte

Account (Billing)

Entité de facturation

Commande

Order

Intention d’achat confirmée

Panier

Cart

Intention d’achat non confirmée

Impact sur le code#

Le langage commun doit être directement visible dans le code. Les noms de classes, méthodes et variables utilisent les termes du domaine :

# Mauvais : vocabulaire technique générique
def process_transaction(user_id, items, payment_info):
    record = db.get("users", user_id)
    for item in items:
        db.update("inventory", item["id"], -item["qty"])
    charge_card(payment_info, sum(i["price"] for i in items))

# Bon : langage du domaine visible dans le code
def place_order(customer: "Customer", cart: "Cart",
                payment: "PaymentMethod") -> "Order":
    order = Order.create_from_cart(cart, customer)
    inventory.reserve_items(order.items)
    payment_service.charge(payment, order.total)
    return order

Conseil pratique

Maintenez un glossaire vivant du langage commun (dans le wiki ou un fichier GLOSSARY.md). Quand un terme change de sens suite à une découverte métier, mettez à jour le code simultanément. Un décalage entre le glossaire et le code est un signe d’alerte majeur.

Simulation : analyse de fréquence de termes métier dans un code#

from collections import Counter
import re

# Simulation d'une base de code e-commerce (noms de symboles extraits)
code_samples = """
class Order:
    def place_order(self, customer, cart): pass
    def confirm_order(self, order_id): pass
    def cancel_order(self, order_id, reason): pass

class Cart:
    def add_to_cart(self, product, quantity): pass
    def remove_from_cart(self, product_id): pass
    def checkout(self, customer, payment_method): pass

class Customer:
    def register_customer(self, email, name): pass
    def update_customer_address(self, customer_id, address): pass

class Product:
    def list_products(self, category): pass
    def search_products(self, query): pass
    def update_product_price(self, product_id, price): pass

class Payment:
    def process_payment(self, order, payment_method): pass
    def refund_payment(self, order_id, amount): pass

class Inventory:
    def reserve_inventory(self, product_id, quantity): pass
    def release_inventory(self, product_id, quantity): pass
    def check_availability(self, product_id): pass

class Shipping:
    def create_shipment(self, order): pass
    def track_shipment(self, tracking_number): pass
    def deliver_shipment(self, shipment_id): pass
"""

# Extraction des termes métier (mots des noms de méthodes/classes)
tokens = re.findall(r'[a-z][a-z]+', code_samples.lower())

# Termes techniques à exclure
stop_words = {'self', 'pass', 'def', 'class', 'return', 'import',
              'from', 'and', 'the', 'for', 'with'}

domain_terms = [t for t in tokens if t not in stop_words and len(t) > 3]
freq = Counter(domain_terms)

print("=== Top 20 termes du domaine dans la base de code ===")
print()
for term, count in freq.most_common(20):
    bar = "█" * count
    print(f"  {term:<20} {bar} ({count})")

print()
print(f"Vocabulaire total unique : {len(freq)} termes")
print(f"Termes apparaissant ≥ 3 fois : {sum(1 for c in freq.values() if c >= 3)}")
=== Top 20 termes du domaine dans la base de code ===

  order                █████████ (9)
  product              ████████ (8)
  customer             ██████ (6)
  payment              █████ (5)
  cart                 ████ (4)
  shipment             ████ (4)
  quantity             ███ (3)
  inventory            ███ (3)
  method               ██ (2)
  update               ██ (2)
  address              ██ (2)
  products             ██ (2)
  price                ██ (2)
  place                █ (1)
  confirm              █ (1)
  cancel               █ (1)
  reason               █ (1)
  remove               █ (1)
  checkout             █ (1)
  register             █ (1)

Vocabulaire total unique : 39 termes
Termes apparaissant ≥ 3 fois : 8

Bounded Context — la frontière explicite du modèle#

Un Bounded Context est une frontière explicite à l’intérieur de laquelle un modèle de domaine particulier s’applique. C’est l’un des outils les plus puissants du DDD stratégique.

Le problème du modèle unique#

Une erreur fréquente dans les projets d’envergure consiste à vouloir un modèle de données unifié pour toute l’entreprise. Cette approche semble logique (un seul « Customer », une seule « Order ») mais échoue en pratique : les différents sous-systèmes ont des besoins incompatibles, et le modèle unique finit par devenir une collection de champs optionnels incompréhensibles.

Même mot, sens différent#

Le terme « Customer » n’a pas le même sens dans tous les sous-systèmes d’un e-commerce :

  • Sales : le client est une personne avec une intention d’achat, un historique de commandes, une segmentation marketing

  • Support : le client est un utilisateur ayant un problème, avec un historique de tickets et un niveau de priorité SLA

  • Billing : le client est une entité juridique avec une adresse de facturation, un mode de paiement et un statut de solvabilité

Forcer ces trois représentations dans un seul objet Customer crée une classe monstrueuse avec des dizaines de champs dont la plupart sont nuls selon le contexte.

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import numpy as np

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, axes = plt.subplots(1, 3, figsize=(15, 7))

contexts = {
    "Sales\nContext": {
        "couleur": "#4C72B0",
        "attributs": [
            "customer_id",
            "full_name",
            "email",
            "phone",
            "loyalty_points",
            "segment",
            "acquisition_source",
            "lifetime_value",
            "preferred_category",
            "last_purchase_date",
        ],
        "cles": ["customer_id", "email"],
    },
    "Support\nContext": {
        "couleur": "#DD8452",
        "attributs": [
            "customer_id",
            "full_name",
            "email",
            "sla_tier",
            "open_tickets",
            "avg_response_time",
            "satisfaction_score",
            "preferred_channel",
            "escalation_history",
            "last_contact_date",
        ],
        "cles": ["customer_id", "sla_tier"],
    },
    "Billing\nContext": {
        "couleur": "#55A868",
        "attributs": [
            "account_id",
            "legal_name",
            "vat_number",
            "billing_address",
            "payment_method",
            "credit_limit",
            "outstanding_balance",
            "invoice_frequency",
            "payment_terms",
            "tax_exempt",
        ],
        "cles": ["account_id", "vat_number"],
    },
}

for ax, (title, data) in zip(axes, contexts.items()):
    color = data["couleur"]
    attributs = data["attributs"]
    cles = data["cles"]

    ax.set_xlim(0, 1)
    ax.set_ylim(0, 1)
    ax.set_facecolor("#F8F9FA")

    # En-tête
    header = mpatches.FancyBboxPatch(
        (0.05, 0.85), 0.9, 0.12,
        boxstyle="round,pad=0.01",
        facecolor=color, edgecolor="white", linewidth=2
    )
    ax.add_patch(header)
    ax.text(0.5, 0.91, f"Customer\n({title})",
            ha="center", va="center", fontsize=10,
            fontweight="bold", color="white")

    # Attributs
    step = 0.072
    for i, attr in enumerate(attributs):
        y = 0.82 - i * step
        is_key = attr in cles
        bg_color = "#FFF3CD" if is_key else "white"
        prefix = "🔑 " if is_key else "   "
        row = mpatches.FancyBboxPatch(
            (0.05, y - 0.025), 0.9, 0.048,
            boxstyle="round,pad=0.005",
            facecolor=bg_color,
            edgecolor="#DDDDDD", linewidth=0.5
        )
        ax.add_patch(row)
        ax.text(0.12, y, f"{prefix}{attr}",
                va="center", fontsize=7.5, color="#333333",
                fontfamily="monospace")

    ax.set_title(title.replace("\n", " "), fontsize=12,
                 fontweight="bold", color=color, pad=8)
    ax.axis("off")

fig.suptitle(
    "Le même mot 'Customer' — trois modèles différents selon le Bounded Context",
    fontsize=13, fontweight="bold", y=1.01
)
plt.savefig("customer_bounded_contexts.png", dpi=120,
            bbox_inches="tight", facecolor="white")
plt.show()
/tmp/ipykernel_83261/1202336693.py:107: UserWarning: Glyph 128273 (\N{KEY}) missing from font(s) DejaVu Sans Mono.
  plt.savefig("customer_bounded_contexts.png", dpi=120,
/home/loc/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170: UserWarning: Glyph 128273 (\N{KEY}) missing from font(s) DejaVu Sans Mono.
  fig.canvas.print_figure(bytes_io, **kw)
_images/fd28ee4ca97b6515c69574165f8a1c02c025a4e98488c8206a96cbacc3fe18e8.png

Règle d’or

Un Bounded Context définit la frontière à l’intérieur de laquelle le langage commun est cohérent. Un même terme peut avoir des significations différentes dans deux Bounded Contexts distincts — c’est non seulement acceptable, mais souhaitable.

Frontières et traductions#

Quand deux Bounded Contexts doivent communiquer, une traduction est nécessaire. Cette traduction peut être explicite (via un objet dédié) ou implicite (via un mapping de données). L’Anti-Corruption Layer (ACL) est le pattern qui formalise cette traduction pour protéger un contexte des concepts d’un autre.

Context Map — les relations entre bounded contexts#

Une Context Map est un diagramme qui documente les relations entre les Bounded Contexts d’un système. Elle révèle la dynamique sociale et technique entre les équipes.

Types de relations#

Partnership : deux équipes collaborent étroitement et évoluent ensemble. Risque : fort couplage organisationnel.

Shared Kernel : deux contextes partagent une portion de modèle commune. Toute modification doit être approuvée par les deux équipes. À utiliser avec parcimonie.

Customer-Supplier : un contexte « fournisseur » produit ce dont le contexte « client » a besoin. Le fournisseur a le pouvoir décisionnel.

Conformist : le contexte client adopte le modèle du fournisseur sans négociation. Souvent le cas avec des systèmes tiers imposés.

Anti-Corruption Layer (ACL) : le contexte client traduit le modèle du fournisseur pour le protéger des concepts étrangers.

Open Host Service (OHS) : le fournisseur expose un service bien défini et documenté, utilisable par plusieurs clients.

Published Language (PL) : le fournisseur publie un format d’échange standardisé (JSON Schema, Protobuf, etc.).

Separate Ways : deux contextes n’ont pas de relation d’intégration. Chacun résout ses problèmes indépendamment.

Context Map d’un e-commerce#

import networkx as nx
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(14, 10))
ax.set_facecolor("#F0F4F8")

# Définition des bounded contexts et leurs positions
contexts = {
    "Catalog": (0.15, 0.75),
    "Cart": (0.40, 0.82),
    "Order\nManagement": (0.65, 0.72),
    "Payment": (0.82, 0.50),
    "Inventory": (0.15, 0.45),
    "Shipping": (0.65, 0.35),
    "Customer\nProfile": (0.40, 0.55),
    "Billing": (0.82, 0.25),
    "Notification": (0.40, 0.22),
    "Analytics": (0.15, 0.20),
}

# Relations : (source, cible, type, label court)
relations = [
    ("Cart", "Catalog", "OHS/PL", "#4C72B0"),
    ("Cart", "Customer\nProfile", "Customer-Supplier", "#DD8452"),
    ("Cart", "Inventory", "Customer-Supplier", "#DD8452"),
    ("Order\nManagement", "Cart", "Customer-Supplier", "#DD8452"),
    ("Order\nManagement", "Payment", "Customer-Supplier", "#DD8452"),
    ("Order\nManagement", "Shipping", "Customer-Supplier", "#DD8452"),
    ("Order\nManagement", "Inventory", "Customer-Supplier", "#DD8452"),
    ("Payment", "Billing", "ACL", "#C44E52"),
    ("Notification", "Order\nManagement", "Conformist", "#8172B2"),
    ("Notification", "Shipping", "Conformist", "#8172B2"),
    ("Analytics", "Order\nManagement", "OHS/PL", "#4C72B0"),
    ("Analytics", "Customer\nProfile", "OHS/PL", "#4C72B0"),
    ("Shipping", "Inventory", "Partnership", "#55A868"),
]

# Dessin des noeuds
for name, (x, y) in contexts.items():
    is_core = name in ["Order\nManagement", "Cart", "Payment"]
    color = "#2C6FAC" if is_core else "#6AAB9C"
    border = "#1A4A7A" if is_core else "#3D7A6E"
    box = mpatches.FancyBboxPatch(
        (x - 0.075, y - 0.04), 0.15, 0.08,
        boxstyle="round,pad=0.01",
        facecolor=color, edgecolor=border, linewidth=2,
        zorder=3
    )
    ax.add_patch(box)
    ax.text(x, y, name, ha="center", va="center",
            fontsize=8, fontweight="bold", color="white",
            zorder=4)

# Dessin des arêtes
for src, dst, rel_type, color in relations:
    x1, y1 = contexts[src]
    x2, y2 = contexts[dst]
    ax.annotate(
        "", xy=(x2, y2), xytext=(x1, y1),
        arrowprops=dict(
            arrowstyle="-|>",
            color=color,
            lw=1.8,
            connectionstyle="arc3,rad=0.1"
        ),
        zorder=2
    )
    mx, my = (x1 + x2) / 2, (y1 + y2) / 2
    ax.text(mx, my + 0.015, rel_type,
            ha="center", va="bottom", fontsize=6.5,
            color=color, fontweight="bold",
            bbox=dict(boxstyle="round,pad=0.1",
                      facecolor="white", edgecolor=color,
                      alpha=0.85, linewidth=0.8))

# Légende
legend_items = [
    mpatches.Patch(color="#4C72B0", label="Open Host Service / Published Language"),
    mpatches.Patch(color="#DD8452", label="Customer-Supplier"),
    mpatches.Patch(color="#C44E52", label="Anti-Corruption Layer"),
    mpatches.Patch(color="#8172B2", label="Conformist"),
    mpatches.Patch(color="#55A868", label="Partnership"),
    mpatches.Patch(color="#2C6FAC", label="Core Domain"),
    mpatches.Patch(color="#6AAB9C", label="Supporting / Generic Domain"),
]
ax.legend(handles=legend_items, loc="lower right",
          fontsize=8, framealpha=0.9)

ax.set_xlim(0, 1)
ax.set_ylim(0.08, 1.0)
ax.set_title("Context Map — Système e-commerce",
             fontsize=14, fontweight="bold", pad=12)
ax.axis("off")
plt.savefig("context_map_ecommerce.png", dpi=120,
            bbox_inches="tight", facecolor="#F0F4F8")
plt.show()
_images/9cfa127b0eaacfaa9823e94c14772313c8ee8d7435ce42e8d8fdaa1bfa4f805f.png

Sous-domaines — où investir le plus#

Le DDD distingue trois types de sous-domaines qui guident les décisions d’investissement et de conception.

Core Domain — le coeur différenciant#

Le Core Domain est ce qui différencie l’entreprise de ses concurrents. C’est là que réside la valeur métier unique, là où les règles sont les plus complexes et les plus volatiles. C’est sur le Core Domain que l’équipe la plus expérimentée doit travailler, avec les outils DDD les plus sophistiqués.

Exemples :

  • Pour un e-commerce : le moteur de recommandation, la tarification dynamique, la gestion des stocks

  • Pour une banque : l’évaluation du risque crédit, la détection de fraude

  • Pour une plateforme de livraison : l’optimisation des routes, la gestion des créneaux

Supporting Domain — le support nécessaire#

Un Supporting Domain supporte le Core Domain sans être lui-même différenciant. Les règles métier sont présentes mais moins complexes. On peut y accepter une qualité légèrement inférieure, voire sous-traiter à des équipes moins expérimentées.

Exemples : gestion des profils utilisateurs, système de notifications, gestion des retours.

Generic Domain — la commodité#

Un Generic Domain est une fonctionnalité commune à de nombreux domaines, sans spécificité métier. Il vaut mieux acheter ou utiliser un service existant plutôt que de le développer en interne.

Exemples : authentification/autorisation, envoi d’emails, génération de PDF, traitement des paiements via Stripe.

Matrice investissement#

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

fig, ax = plt.subplots(figsize=(12, 8))

sous_domaines = [
    # (nom, valeur_metier, complexite, type, taille)
    ("Recommandation\nproduits",        9.2, 8.5, "Core",       500),
    ("Tarification\ndynamique",          8.8, 9.0, "Core",       500),
    ("Gestion des\nstocks",              8.0, 7.5, "Core",       500),
    ("Détection\nfraude",                9.5, 9.2, "Core",       500),
    ("Gestion\ncommandes",               7.5, 6.8, "Core",       500),
    ("Profils\nutilisateurs",            5.0, 4.0, "Supporting", 300),
    ("Gestion\nretours",                 5.5, 5.2, "Supporting", 300),
    ("Programme\nfidélité",              6.5, 5.8, "Supporting", 300),
    ("Notifications",                    3.5, 2.8, "Generic",    200),
    ("Authentification",                 3.0, 3.5, "Generic",    200),
    ("Envoi\nd'emails",                  2.5, 1.5, "Generic",    200),
    ("Génération\nde PDF",               2.0, 1.8, "Generic",    200),
    ("Paiements\n(via Stripe)",          4.0, 2.5, "Generic",    200),
]

colors = {"Core": "#C44E52", "Supporting": "#DD8452", "Generic": "#4C72B0"}
labels_done = set()

for nom, valeur, complexite, type_sd, taille in sous_domaines:
    color = colors[type_sd]
    label = type_sd if type_sd not in labels_done else None
    labels_done.add(type_sd)
    ax.scatter(complexite, valeur, s=taille, c=color,
               alpha=0.75, edgecolors="white", linewidth=1.5,
               label=label, zorder=3)
    ax.text(complexite + 0.08, valeur, nom,
            fontsize=7.5, va="center", color="#333333")

# Zones de fond
ax.axhspan(7, 10, alpha=0.04, color="#C44E52")
ax.axhspan(4, 7, alpha=0.04, color="#DD8452")
ax.axhspan(0, 4, alpha=0.04, color="#4C72B0")

# Annotations stratégiques
ax.text(9.5, 9.8, "Investir fortement\n(équipe experte, DDD complet)",
        ha="right", fontsize=8, color="#C44E52",
        style="italic",
        bbox=dict(boxstyle="round", facecolor="white",
                  edgecolor="#C44E52", alpha=0.7))
ax.text(9.5, 6.8, "Équipe intermédiaire,\npatterns simples",
        ha="right", fontsize=8, color="#DD8452",
        style="italic",
        bbox=dict(boxstyle="round", facecolor="white",
                  edgecolor="#DD8452", alpha=0.7))
ax.text(9.5, 3.5, "Acheter / SaaS\n(ne pas réinventer la roue)",
        ha="right", fontsize=8, color="#4C72B0",
        style="italic",
        bbox=dict(boxstyle="round", facecolor="white",
                  edgecolor="#4C72B0", alpha=0.7))

ax.set_xlabel("Complexité technique", fontsize=11)
ax.set_ylabel("Valeur métier différenciante", fontsize=11)
ax.set_title("Matrice d'investissement — Core / Supporting / Generic Domain",
             fontsize=13, fontweight="bold")
ax.set_xlim(0, 10.5)
ax.set_ylim(0, 10.5)
ax.legend(title="Type de sous-domaine", fontsize=9, title_fontsize=9)
plt.savefig("investissement_sous_domaines.png", dpi=120,
            bbox_inches="tight", facecolor="white")
plt.show()
_images/b471a1213fa49b5f8574b0b57ae34965981a00ae44b1144f38ed68a55e5e5768.png

Piège fréquent

Beaucoup d’équipes traitent leur Core Domain comme un Generic Domain (en achetant un ERP générique) ou leur Generic Domain comme un Core Domain (en réécrivant leur propre système d’authentification). Les deux erreurs coûtent cher : dans le premier cas, on perd son avantage concurrentiel ; dans le second, on gaspille des ressources précieuses.

Strangler Fig appliqué au DDD#

Le pattern Strangler Fig, proposé par Martin Fowler, décrit une stratégie de migration progressive d’un système legacy vers une nouvelle architecture. Appliqué au DDD, il permet d’extraire progressivement des Bounded Contexts depuis un monolithe existant.

Principe#

Comme le figuier étrangleur qui pousse autour d’un arbre hôte et finit par le remplacer, la nouvelle architecture s’installe progressivement autour du legacy. À aucun moment le système n’est indisponible, et chaque étape apporte de la valeur.

Étapes de migration#

Étape 1 — Identifier les Bounded Contexts dans le legacy#

Avant de couper quoi que ce soit, on identifie les frontières naturelles dans le code existant. Souvent, les modules du legacy correspondent approximativement aux Bounded Contexts futurs, même s’ils sont entrelacés.

Étape 2 — Choisir le premier contexte à extraire#

On commence par un contexte à faible risque mais à valeur démontrable, ou par le contexte qui change le plus fréquemment (et dont le legacy devient un goulot d’étranglement).

Étape 3 — Installer un façade/proxy#

Un composant de routage est installé devant le legacy. Au début, tout le trafic passe par le legacy. Progressivement, les routes sont redirigées vers les nouveaux services.

Étape 4 — Synchronisation des données#

Pendant la migration, les deux systèmes coexistent. Un mécanisme de synchronisation (souvent via événements) maintient la cohérence. L’ACL traduit les concepts entre l’ancien et le nouveau modèle.

Étape 5 — Désactivation progressive du legacy#

Une fois qu’un contexte est entièrement migré et validé, la portion correspondante du legacy est désactivée.

# Exemple : façade Strangler Fig pour le contexte Order
class OrderFacade:
    """
    Façade qui route vers le legacy ou le nouveau service
    selon l'état de la migration.
    """
    def __init__(self, legacy_service, new_order_service,
                 feature_flags):
        self._legacy = legacy_service
        self._new = new_order_service
        self._flags = feature_flags

    def place_order(self, request: dict) -> dict:
        if self._flags.is_enabled("new_order_service"):
            # Traduction ACL : format legacy → modèle DDD
            command = PlaceOrderCommand(
                customer_id=request["userId"],
                items=[
                    OrderItemDto(
                        product_id=item["productId"],
                        quantity=item["qty"]
                    )
                    for item in request["cart"]["items"]
                ]
            )
            return self._new.handle(command)
        else:
            return self._legacy.create_order(request)

Patience requise

Une migration Strangler Fig bien conduite sur un système de taille moyenne prend 12 à 36 mois. Accepter cette durée dès le départ évite les demi-mesures et les abandons prématurés qui laissent le système dans un état hybride ingérable.

Exemples complets — système e-commerce décomposé#

Décomposition en Bounded Contexts#

Un système e-commerce de taille moyenne se décompose typiquement en 8 à 12 Bounded Contexts. Voici une décomposition raisonnée avec les responsabilités et les frontières explicites :

Bounded Context

Responsabilité principale

Type

Équipe

Product Catalog

Référentiel produits, catégories, attributs

Supporting

Équipe produit

Cart & Checkout

Session d’achat, sélection, calcul prix

Core

Équipe achat

Order Management

Cycle de vie des commandes, états, règles

Core

Équipe commandes

Payment

Orchestration des paiements, remboursements

Core

Équipe paiement

Inventory

Disponibilité, réservations, alertes stock

Core

Équipe stock

Shipping

Acheminement, transporteurs, suivi

Supporting

Équipe logistique

Customer Profile

Données clients, préférences, historique

Supporting

Équipe CRM

Billing

Facturation, comptabilité, TVA

Supporting

Équipe finance

Notification

Emails, SMS, push — sans logique métier

Generic

Ops

Analytics

Reporting, métriques — lecture seule

Generic

Data team

Intégration : les anti-patterns à éviter#

Base de données partagée : deux Bounded Contexts qui accèdent aux mêmes tables de base de données. La frontière est illusoire — toute modification de schéma impacte les deux contextes.

Appels synchrones en chaîne : Order Management → Inventory → Warehouse → Shipping → Notification. Un seul maillon défaillant interrompt toute la chaîne. Préférer l’orchestration asynchrone via événements.

Entités partagées : une classe Customer importée d’un module « commun » utilisée dans tous les contextes. Chaque contexte doit avoir sa propre représentation locale, même si elle est plus petite.

Résumé#

Le DDD stratégique fournit un ensemble d’outils pour dompter la complexité à grande échelle :

  • L”Ubiquitous Language aligne la compréhension entre experts métier et développeurs, et rend le code lisible par le domaine.

  • Les Bounded Contexts définissent des frontières explicites à l’intérieur desquelles le modèle est cohérent, permettant à des équipes différentes de travailler indépendamment.

  • La Context Map documente les relations entre contextes : qui décide, qui s’adapte, qui traduit.

  • La classification Core / Supporting / Generic guide les décisions d’investissement : où mettre les meilleurs développeurs, où acheter une solution prête à l’emploi.

  • Le Strangler Fig rend la migration d’un legacy praticable sans réécriture from scratch.

À retenir

Le DDD stratégique n’est pas une architecture — c’est une méthode de découverte et de cartographie du domaine. L’architecture (hexagonale, événementielle, microservices) vient ensuite, à l’intérieur des Bounded Contexts identifiés. Commencer par l’architecture avant d’avoir compris le domaine est l’erreur la plus coûteuse en DDD.

Les outils tactiques — entités, value objects, agrégats, repositories, domain events — permettent d’implémenter rigoureusement ces Bounded Contexts. C’est l’objet du chapitre suivant.