Conception détaillée d'une application — Modélisation et architecture interne
Après avoir défini les objectifs métier et découpé le système en Bounded Contexts, l'étape suivante consiste à descendre au niveau de la conception détaillée : comment concevoir l'intérieur de chaque domaine pour garantir robustesse, maintenabilité et clarté ?
Reprenons notre fil conducteur e-commerce (Prise de commande) pour explorer les décisions de conception détaillée au cœur du système.
🏗️ 1. Modélisation tactique DDD : Agrégats, Entités & Value Objects
Dans un domaine métier, tout n'a pas la même nature. La conception détaillée distingue précisément :
Objets immuables définis uniquement par leurs valeurs (pas d'identifiant technique). Ils encapsulent la validation et empêchent les états incohérents dès l'instanciation.
🛒 Exemples : Money(amount: 199.99, currency: EUR), Quantity(2), Email("client@domain.com").
Objets possédant une identité unique continue dans le temps, même si leurs attributs changent au fil du cycle de vie.
🛒 Exemple : OrderLine(id: "item-12", productId: "PROD-98", quantity: 2, unitPrice: 199.99€).
La grappe d'objets traitée comme une unité transactionnelle indivisible. C'est la seule porte d'entrée : aucun composant externe ne modifie les objets internes directement.
🛒 Exemple : Order ou Cart sont des racines qui contrôlent leurs lignes internes, recalculent leurs totaux et valident les transitions.
🏷️ Diagramme de classes métier & classification tactique
Voici le diagramme de classes complet intégrant l'ensemble du cycle de vente : du Catalogue Produit et du Panier (Cart) jusqu'à la Commande (Order) et ses contextes satellites. Chaque classe arbore son stéréotype tactique explicite : «Aggregate Root», «Entity» ou «Value Object».
📐 Règles d'invariants et frontières transactionnelles
Comme illustré dans le diagramme de classes ci-dessus :
- Cycle Panier $\rightarrow$ Commande (
CartvsOrder) :- Le Panier (
Cart) est un agrégat volatile et permissif (ajout/retrait sans réservation définitive). - Lors de la validation (Checkout), le panier se fige et instancie l'agrégat Commande (
Order) avec ses invariants stricts (prix garantis, adresses validées, vérification des plafonds).
- Le Panier (
- Découplage avec le Catalogue (
CatalogProduct) :- L'agrégat
OrderLineetCartItemne référencent le produit que par sonProductIdet copient son prix instantané dans unMoneyimmuable. Si le catalogue modifie le tarif le lendemain, les commandes antérieures restent inchangées.
- L'agrégat
- Intégrité forte dans chaque agrégat :
- Les entités internes (
OrderLine,CartItem,AppliedPromotion) ne sont accessibles et modifiables que par leurs racines respectives (Order,Cart).
- Les entités internes (
- Découplage inter-contextes (Customer, Stock, Payment) :
- Les interactions entre contextes ne passent jamais par des références d'objets en mémoire mais par des identifiants fortement typés (
CustomerId,ProductId,OrderId) et des événements de domaine asynchrones.
- Les interactions entre contextes ne passent jamais par des références d'objets en mémoire mais par des identifiants fortement typés (
🎯 2. Conception des contrats et flux de données (Ports & DTOs)
La conception détaillée formalise précisément la frontière entre l'extérieur et le cœur métier grâce à des contrats explicites :
1. Flux architectural : DTO d'entrée ➔ Use Case ➔ Domaine (Objets Métier) ➔ DTO de sortie
Une DTO d'entrée (Data Transfer Object) transmet les données brutes depuis l'extérieur (API REST, message broker, UI) vers le Use Case sans comporter de logique métier. Le Use Case orchestre l'exécution, charge et manipule les Objets Métier du Domaine (Racines d'agrégat, Entités, Objets-Valeurs), applique les règles d'invariants, puis projette le résultat final dans une DTO de sortie immuable.
🔒 3. Gestion de la concurrence et transactions
Dans une application à forte charge (ex: fortes ventes de produits limités ou mises à jour concurrentes d'un panier/commande), la conception détaillée doit anticiper les conflits d'accès et garantir la cohérence des données.
🛡️ Le Verrouillage Optimiste (Optimistic Locking) avec numéro de version
Le verrouillage optimiste part du principe que les conflits concurrents sont rares, évitant ainsi de poser des verrous bloquants en base (comme un SELECT FOR UPDATE qui dégrade fortement le débit).
1. Principe de fonctionnement
- Lecture initiale : Chaque agrégat métier (ex:
Order,Cart,InventoryItem) possède un attributversion: numberpersisté en base de données (initialisé à1). Lorsque la requête charge l'agrégat, elle lit l'état actuel et capture sa version (ex:version = 1). - Mutation en mémoire : Le Use Case applique les règles métier et incrémente la version attendue (
version = 2). - Écriture atomique conditionnelle : Lors de la sauvegarde (
UPDATE), une clause conditionnelle vérifie strictement que la version en base n'a pas bougé depuis la lecture :UPDATE orders SET status = 'PAID', version = version + 1 WHERE id = :id AND version = 1; - Détection du conflit :
- Requête gagnante (Requête A) : La ligne est trouvée (
row_count = 1), l'écriture est validée, la version passe à2. - Requête concurrente (Requête B) : La version en base valant désormais
2, la conditionWHERE version = 1ne correspond à aucun enregistrement (row_count = 0). La base rejette l'écriture sans bloquer les autres transactions.
- Requête gagnante (Requête A) : La ligne est trouvée (
- Gestion de l'exception métier : L'ORM ou le Repository lève une exception dédiée (ex:
OptimisticLockException). Le système peut alors au choix :- Rejouer automatiquement le Use Case (Retry) avec les nouvelles données fraîches.
- Retourner une réponse HTTP
409 Conflictau client avec l'état à jour.
2. Schéma séquentiel du Verrouillage Optimiste
🔑 L'Idempotence (Idempotency Key) et déduplication des requêtes
Dans les architectures web et distribuées, les défaillances réseau (timeouts, déconnexions) ou les doubles clics utilisateurs entraînent fréquemment la réémission d'une même commande de modification (POST /v1/orders, POST /v1/payments). Sans idempotence, le client risquerait d'être débité deux fois ou d'engendrer deux commandes identiques.
L'idempotence garantit qu'exécuter une opération une ou plusieurs fois produit exactement le même état métier sur le système.
1. Principe de fonctionnement
- Émission de la clé unique : Le client génère un UUID aléatoire et l'envoie dans un en-tête HTTP dédié :
Idempotency-Key: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d. - Verrou atomique d'enregistrement : Dès réception, un middleware ou l'intercepteur applicatif tente d'insérer cette clé dans une table ou un store dédié (
idempotency_keys) de manière atomique :INSERT INTO idempotency_records (key, request_hash, status, created_at) VALUES (:idempotency_key, :hash, 'IN_PROGRESS', NOW()) ON CONFLICT (key) DO NOTHING; - Arbitrage selon l'état de la clé :
- Premier appel (
IN_PROGRESS) : La ligne est insérée avec succès. Le système sait qu'il s'agit du traitement légitime et exécute le Use Case métier (réservation stock, débit bancaire). - Appel concurrent en cours : Si une requête avec la même clé arrive alors que le statut est toujours
IN_PROGRESS, le serveur renvoie immédiatement une erreur409 Conflict(ou425 Too Early) indiquant qu'un traitement identique est déjà en cours. - Appel rejoué après succès (
COMPLETED) : Si la clé existe déjà avec le statutCOMPLETED, le moteur applicatif court-circuite complètement le Use Case : aucun recalcul ni prélèvement n'est relancé. Il lit la réponse mise en cache (response_code,response_body) et la restitue à l'identique.
- Premier appel (
- Finalisation transactionnelle : Une fois la commande persistée avec succès, le statut passe à
COMPLETEDavec son corps de réponse JSON et un délai d'expiration (TTL de 24h à 7 jours).
2. Schéma séquentiel de l'Idempotence
📮 Le Pattern Outbox Transactionnelle (Transactional Outbox)
Dans une architecture distribuée ou orientée événements (Event-Driven), un cas classique de défaillance est le problème de double écriture non atomique (Dual-Write Problem).
1. Le problème de la double écriture (Dual-Write Problem)
Lorsqu'un Use Case valide une commande, deux opérations doivent obligatoirement réussir de concert :
- Écrire en base de données locale : Sauvegarder la commande (
INSERT INTO orders). - Publier un événement métier : Notifier Kafka, RabbitMQ ou AWS SQS (
publisher.send(new OrderPaidEvent())).
Si le serveur plante ou le réseau coupe après le commit SQL mais avant la publication du message, l'événement est définitivement perdu et les systèmes avals (facturation, expédition, notification client) ne seront jamais déclenchés. À l'inverse, si l'on publie le message avant et que le commit SQL échoue, les services consommateurs traitent un événement fantôme qui n'existe pas en base.
Le Pattern Outbox Transactionnelle résout ce dilemme en transformant la publication d'événement en une écriture locale dans la même transaction ACID.
2. Principe de fonctionnement étape par étape
- Écriture atomique combinée : Dans une unique transaction SQL, le Use Case insère à la fois l'agrégat métier dans la table
orderset l'événement correspondant dans une table localeoutbox_events:
Garantie ACID : Soit la commande et l'événement sont tous les deux persistés, soit aucun des deux ne l'est.BEGIN TRANSACTION; INSERT INTO orders (id, customer_id, total, status) VALUES ('CMD-42', 'CUST-1', 99.00, 'PAID'); INSERT INTO outbox_events (id, aggregate_type, aggregate_id, event_type, payload, status, created_at) VALUES (gen_random_uuid(), 'Order', 'CMD-42', 'OrderPaidEvent', '{"orderId":"CMD-42","amount":99.00}', 'PENDING', NOW()); COMMIT; - Découplage de la publication (Outbox Relayer / Message Relay) :
Un processus d'arrière-plan asynchrone (worker dédié via polling périodique ou moteur de CDC comme Debezium lisant le transaction log de la base) scrute les lignes
PENDINGde la tableoutbox_events. - Publication sur le Message Broker : Le relayer envoie le message vers le broker (Kafka, RabbitMQ, EventBridge).
- Acquittement ou archivage :
Dès réception de l'accusé de réception (ACK) du broker, le worker met à jour la ligne outbox à
PUBLISHED(ou supprime la ligne) pour garantir une sémantique de livraison au moins une fois (At-Least-Once Delivery).