Pourquoi le CRUD ordinaire tue votre projet, et comment le DDD peut y remédier
\n\nChaque développeur a déjà été confronté à une situation où un projet, simple à première vue, se transforme en un « marécage » de services, de contrôleurs et de logique métier dispersée dans tout le code. Vous commencez à écrire UserService, puis OrderService, et six mois plus tard, OrderService contient la logique d'envoi d'email, de calcul des remises et de vérification du solde utilisateur. Cela vous dit quelque chose ?
Le problème est que nous pensons trop tôt aux bases de données, aux frameworks et aux API REST. Nous oublions l'essentiel — le domaine métier (domain). Le Domain-Driven Design (DDD) propose une approche radicalement différente : d'abord, nous modélisons les processus métier dans le langage des experts, et ensuite seulement, le code.
\n\nDans cet article, nous allons voir comment appliquer le Domain-Driven Design en TypeScript avec des exemples concrets. Vous découvrirez comment construire une architecture propre où les règles métier vivent séparément de l'infrastructure, et où le code est facile à tester et à modifier.
\n\nFondamentaux du DDD : langage, contextes et modèle
\n\nUbiquitous Language (Langage omniprésent)
\nLe premier principe du DDD est un langage commun, compréhensible à la fois par les développeurs et les experts métier. Si le manager dit « Le client a activé son abonnement », alors dans le code, il doit y avoir une méthode activateSubscription(), et non setStatus(1). TypeScript, avec son typage strict, est idéal pour cela — les noms des classes, méthodes et variables deviennent une documentation.
Bounded Context (Contexte délimité)
\nUn grand système est divisé en contextes indépendants : « Paiements », « Gestion des utilisateurs », « Livraison ». Chaque contexte a son propre modèle et son propre langage. Les modules TypeScript et les types stricts aident à séparer rigoureusement ces contextes au niveau des imports.
\n\nConstruire un modèle domaine en TypeScript
\n\nValue Object (Objet-valeur)
\nIl s'agit d'une entité immuable définie par ses attributs, et non par un identifiant. Exemple : Email, Money, Coordinates.
\n\n// value-objects/Email.ts\nexport class Email {\n private readonly value: string;\n\n constructor(email: string) {\n if (!this.isValid(email)) {\n throw new Error('Invalid email address');\n }\n this.value = email;\n }\n\n private isValid(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n }\n\n public getValue(): string {\n return this.value;\n }\n\n public equals(other: Email): boolean {\n return this.value === other.value;\n }\n}\n\n\nRemarquez : Email vérifie lui-même sa validité. Aucun service ne doit savoir à quoi ressemble un email valide. C'est du Domain-Driven Design en TypeScript pur — la règle métier vit à l'intérieur de l'objet.
Entity (Entité)
\nContrairement au Value Object, une entité possède un identifiant (ID) et peut évoluer dans le temps. Exemple : User, Order.
\n\n// entities/User.ts\nimport { Email } from '../value-objects/Email';\nimport { UserId } from '../value-objects/UserId';\n\nexport class User {\n private readonly id: UserId;\n private email: Email;\n private isActive: boolean;\n\n constructor(id: UserId, email: Email) {\n this.id = id;\n this.email = email;\n this.isActive = true;\n }\n\n public getId(): UserId {\n return this.id;\n }\n\n public changeEmail(newEmail: Email): void {\n if (!this.isActive) {\n throw new Error('Cannot change email for inactive user');\n }\n this.email = newEmail;\n }\n\n public deactivate(): void {\n this.isActive = false;\n }\n}\n\n\nAggregate (Agrégat)
\nUn agrégat est un groupe d'entités et d'objets-valeurs traités comme un tout. L'agrégat a une racine (Aggregate Root) par laquelle l'accès aux objets internes s'effectue.
\n\nExemple : une commande (Order)