Aller au contenu

Cas avances

DDD, CQRS, event sourcing et refactoring legacy — des patterns pour les systèmes complexes qui ont prouve leur utilité à grande échelle.


Complexité accidentelle

Ces patterns résolvent des problèmes réels — mais seulement quand ces problèmes existent. Introduire DDD dans une application CRUD de 10 tables ou CQRS dans un système avec 100 requêtes/jour ajoute de la complexité sans valeur. Lisez, comprenez, puis attendez le bon moment.


Domain-Driven Design (DDD)

DDD (Eric Evans, 2003) est une approche de conception centree sur le domaine métier. Son objectif : que le code parle le même langage que les experts métier.

Langage ubiquitaire

Tout le monde — développeurs, product owners, experts métier — utilise le même vocabulaire. Ce vocabulaire se retrouve dans le code.

# Mauvais : vocabulaire technique, pas metier
class Data:
    def process(self): ...

class Manager:
    def handle(self, data): ...

# Bon : vocabulaire du domaine
class Loan:
    def approve(self, underwriter: Underwriter): ...
    def disburse(self, account: BankAccount): ...

class RiskAssessment:
    def evaluate(self, application: LoanApplication) -> RiskScore: ...

Bounded Contexts

Un grand système est divise en contextes bornes — chacun avec son propre modèle cohérent.

graph LR
    subgraph Catalog["Catalog Context"]
        P1["Product\n(name, description, price)"]
    end
    subgraph Orders["Orders Context"]
        P2["Product\n(product_id, unit_price)"]
    end
    subgraph Shipping["Shipping Context"]
        P3["Package\n(weight, dimensions)"]
    end
    Catalog -- "Anti-Corruption Layer" --> Orders
    Orders -- "Domain Event" --> Shipping

Le même concept "Produit" a une représentation différente dans chaque contexte. C'est normal et souhaitable — forcer un modèle unifie crée un compromis qui sert mal tous les contextes.

Agregats

Un agregat est un cluster d'objets traites comme une unite pour les modifications. Il a une racine d'agregat — le seul objet accessible depuis l'extérieur.

# Order est la racine d'agregat — OrderItem ne s'acces que via Order
class Order:
    def __init__(self, id: str, customer_id: str):
        self.id = id
        self.customer_id = customer_id
        self._items: list[OrderItem] = []
        self._version = 0

    def add_item(self, product_id: str, qty: int, price: float):
        if self.status != "draft":
            raise DomainError("Impossible d'ajouter un article a une commande confirmee")
        # invariant verifie a l'interieur de l'agregat
        self._items.append(OrderItem(product_id, qty, price))

    def confirm(self):
        if not self._items:
            raise DomainError("Commande vide")
        if self.total() <= 0:
            raise DomainError("Total invalide")
        self.status = "confirmed"
        return [OrderConfirmed(order_id=self.id, total=self.total())]

CQRS — Command Query Responsibility Segregation

Séparer les opérations de lecture (Query) des opérations d'écriture (Command).

graph LR
    Client --> CMD["Command\n(create, update, delete)"]
    Client --> QRY["Query\n(get, list, search)"]
    CMD --> WM["Write Model\nDomain Objects"]
    QRY --> RM["Read Model\nDenormalized Views"]
    WM --> DB1["Write DB"]
    RM --> DB2["Read DB / Cache"]
# Commands — modifient l'etat, retournent des evenements
@dataclass
class CreateOrderCommand:
    customer_id: str
    items: list[dict]

class CreateOrderHandler:
    def __init__(self, repo: OrderRepository, bus: EventBus):
        self._repo = repo
        self._bus = bus

    def handle(self, cmd: CreateOrderCommand) -> str:
        order = Order.create(cmd.customer_id, cmd.items)
        events = order.confirm()
        self._repo.save(order)
        for event in events:
            self._bus.publish(event)
        return order.id

# Queries — lisent l'etat, ne modifient rien
@dataclass
class GetOrderSummaryQuery:
    order_id: str

class GetOrderSummaryHandler:
    def __init__(self, read_db):
        self._db = read_db

    def handle(self, query: GetOrderSummaryQuery) -> dict:
        # requete directe sur une vue denormalisee — pas de domain objects
        return self._db.fetch_one(
            "SELECT * FROM order_summaries WHERE id = ?",
            (query.order_id,)
        )

Quand CQRS est justifie :

  • Modèles de lecture et d'écriture très différents
  • Scalabilité asymetrique (beaucoup plus de lectures que d'écritures)
  • Besoin de plusieurs projections du même état

Quand l'éviter : applications CRUD simples, équipes petites, pas de divergence lecture/écriture.


Event Sourcing

Plutôt que de stocker l'état courant, on stocke la sequence d'événements qui ont produit cet état.

# Sans event sourcing — on stocke l'etat final
orders table:
  id=1, status="shipped", total=99.90, updated_at=...

# Avec event sourcing — on stocke les evenements
events table:
  order_id=1, type="OrderCreated",   data={...}, ts=T1
  order_id=1, type="ItemAdded",      data={...}, ts=T2
  order_id=1, type="OrderConfirmed", data={...}, ts=T3
  order_id=1, type="OrderShipped",   data={...}, ts=T4
class Order:
    def __init__(self):
        self._events: list = []
        self.status = None
        self.items = []

    @classmethod
    def reconstitute(cls, events: list) -> "Order":
        order = cls()
        for event in events:
            order._apply(event)
        return order

    def _apply(self, event):
        handlers = {
            "OrderCreated": self._on_created,
            "ItemAdded": self._on_item_added,
            "OrderConfirmed": self._on_confirmed,
        }
        handlers[event["type"]](event["data"])

    def _on_created(self, data):
        self.id = data["id"]
        self.status = "draft"

    def _on_confirmed(self, data):
        self.status = "confirmed"

Avantages : audit log complet, capacité a rejouer l'historique, debugging facilite, possibilite de construire de nouvelles projections.

Complexité : gestion des schémas d'événements, eventual consistency, snapshots nécessaires sur longues sequences.


Refactoring legacy — patterns de migration

Strangler Fig Pattern

Remplacer incrementalement un système legacy sans big bang.

graph TD
    C["Client"] --> F["Facade / Proxy"]
    F -- "nouvelles features" --> N["Nouveau systeme"]
    F -- "features non migrees" --> L["Legacy"]
    N --> L2["Nouvelle DB"]
  1. Créer une façade qui route vers le legacy
  2. Migrer feature par feature vers le nouveau système
  3. Quand tout est migré, supprimer le legacy

Anti-Corruption Layer

Traduire entre le modèle du nouveau système et celui du legacy pour les isoler.

# Le legacy a un modele pollution dans votre nouveau code
# L'ACL traduit sans contaminer

class LegacyOrderAdapter:
    """Anti-Corruption Layer : isole notre domaine du modele legacy."""

    def __init__(self, legacy_client):
        self._legacy = legacy_client

    def get_order(self, order_id: str) -> Order:
        # le legacy retourne un dict avec des champs cryptiques
        raw = self._legacy.fetch_order_data(order_id)
        # on traduit vers notre modele propre
        return Order(
            id=raw["ORD_NUM"],
            customer_id=raw["CUST_REF"],
            status=self._map_status(raw["STAT_CODE"]),
        )

    def _map_status(self, legacy_code: str) -> str:
        mapping = {"01": "draft", "02": "confirmed", "05": "shipped"}
        return mapping.get(legacy_code, "unknown")

Tableau de référence

Pattern Complexité Benefice principal Quand adopter
DDD (tacticiel) Moyenne Alignement code/métier Domaine complexe, équipe stable
Bounded Contexts Moyenne Isolation des modèles Système avec plusieurs sous-domaines
CQRS Élevée Scalabilité asymetrique Lectures >> écritures, vues multiples
Event Sourcing Très élevée Audit complet, replay Finance, conformité, debug critique
Strangler Fig Faible Migration sans risque Remplacement incremental de legacy
Anti-Corruption Layer Faible Isolation du legacy Intégration de systèmes tiers ou anciens

La complexité se paie

CQRS avec Event Sourcing sur une application de blog est de la complexité accidentelle pure. Ces patterns ont été valides dans des contextes ou la complexité métier etait réelle : systèmes bancaires, e-commerce à grande échelle, applications d'audit. Evaluez le coût avant le benefice.