Architecture hexagonale - 02 - Le domaine : ta logique métier sans dépendances

Isoler la logique métier dans le domaine. Entités, value objects et regles métier en TypeScript, sans framework ni base de donnees.

02 - Le domaine : ta logique métier sans dépendances

Ce que tu vas apprendre

  • Ce que contient (et ne contient pas) la couche domaine
  • Comment écrire des entités et value objects en TypeScript
  • Pourquoi le domaine ne doit jamais appeler le monde extérieur

Prerequisites


La regle d'or du domaine

Ouvre le dossier domain/ de ton projet. Fais un grep sur tous les imports. Si tu vois express, prisma, pg, axios, stripe, nodemailer ou n'importe quelle librairie d'infrastructure, tu as un problème.

Le domaine ne depend de rien d'extérieur. Zero. Nada. Il importe uniquement ses propres types, ses propres interfaces, et eventuellement des utilitaires purs (une lib de validation de format, un generateur d'UUID).

Ca parait restrictif. En pratique, c'est liberateur. Ton domaine devient un module autonome que tu peux tester, lire et raisonner dessus sans connaître le reste du système.

Les entités

Une entité, c'est un objet avec une identité. Deux commandes avec le meme contenu mais des IDs différents sont deux commandes différentes. (Si tu as lu la serie Domaines et cycles de vie, tu connais deja le concept.)

Voici une entité Order en TypeScript :

typescript// domain/entities/Order.ts

type OrderStatus = "draft" | "confirmed" | "paid" | "shipped" | "cancelled";

interface OrderItem {
  productId: string;
  name: string;
  quantity: number;
  unitPrice: number;
}

class Order {
  constructor(
    public readonly id: string,
    public readonly customerId: string,
    private items: OrderItem[],
    private status: OrderStatus,
    public readonly createdAt: Date
  ) {}

  getStatus(): OrderStatus {
    return this.status;
  }

  getItems(): ReadonlyArray<OrderItem> {
    return this.items;
  }

  totalPrice(): number {
    return this.items.reduce(
      (sum, item) => sum + item.quantity * item.unitPrice,
      0
    );
  }

  confirm(): void {
    if (this.status !== "draft") {
      throw new Error(
        `Impossible de confirmer une commande en statut "${this.status}"`
      );
    }
    if (this.items.length === 0) {
      throw new Error("Impossible de confirmer une commande vide");
    }
    this.status = "confirmed";
  }

  markAsPaid(): void {
    if (this.status !== "confirmed") {
      throw new Error("La commande doit etre confirmee avant le paiement");
    }
    this.status = "paid";
  }

  cancel(): void {
    if (this.status === "shipped") {
      throw new Error("Impossible d'annuler une commande deja expediee");
    }
    if (this.status === "cancelled") {
      throw new Error("La commande est deja annulee");
    }
    this.status = "cancelled";
  }

  addItem(item: OrderItem): void {
    if (this.status !== "draft") {
      throw new Error("Ajout d'articles possible uniquement en brouillon");
    }
    if (item.quantity <= 0) {
      throw new Error("La quantite doit etre positive");
    }
    if (item.unitPrice < 0) {
      throw new Error("Le prix unitaire ne peut pas etre negatif");
    }
    this.items.push(item);
  }
}

Regarde bien ce code. Pas un seul import externe. Pas de decorateur @Entity(). Pas de extends BaseModel. C'est du TypeScript pur avec des regles métier.

Les transitions d'état sont explicites : une commande en brouillon peut etre confirmee, une commande confirmee peut etre payee, une commande expediee ne peut pas etre annulee. Ces regles vivent dans le domaine, pas dans un middleware Express ni dans un trigger SQL.

Les value objects

Un value object, c'est un objet sans identité propre. Deux adresses identiques sont la meme adresse. On les utilise pour typer des concepts métier :

typescript// domain/value-objects/Money.ts

class Money {
  constructor(
    public readonly amount: number,
    public readonly currency: string
  ) {
    if (amount < 0) {
      throw new Error("Le montant ne peut pas etre negatif");
    }
    if (!["EUR", "USD", "GBP"].includes(currency)) {
      throw new Error(`Devise non supportee : ${currency}`);
    }
  }

  add(other: Money): Money {
    if (this.currency !== other.currency) {
      throw new Error("Impossible d'additionner des devises differentes");
    }
    return new Money(this.amount + other.amount, this.currency);
  }

  equals(other: Money): boolean {
    return this.amount === other.amount && this.currency === other.currency;
  }
}

Les value objects sont immutables. add() retourne un nouveau Money, il ne modifie pas l'existant. C'est un choix delibere : l'immutabilité elimine toute une categorie de bugs.

Les regles métier comme fonctions pures

Parfois la logique métier ne tient pas dans une entité. Un calcul de frais de livraison qui depend du poids total, de la destination et du type de client, par exemple. Dans ce cas, une fonction pure fait le travail :

typescript// domain/services/ShippingCalculator.ts

function calculateShipping(
  totalWeight: number,
  destination: "france" | "europe" | "international",
  isPremiumCustomer: boolean
): number {
  if (isPremiumCustomer && destination === "france") return 0;

  const baseRate: Record<string, number> = {
    france: 5.99,
    europe: 12.99,
    international: 24.99,
  };

  const weightSurcharge = totalWeight > 10 ? (totalWeight - 10) * 0.5 : 0;
  return baseRate[destination] + weightSurcharge;
}

Pas de base de donnees. Pas d'appel API. Entree, sortie, logique. Ce genre de fonction se teste en une ligne.

Ce que le domaine ne fait PAS

Je déconseillé de mettre quoi que ce soit de technique dans le domaine. Concretement :

  • Le domaine ne sauvegarde pas en base (il ne sait meme pas qu'une base existe)
  • Le domaine n'envoie pas d'email (il peut dire "un email doit etre envoye", via un port sortant)
  • Le domaine ne parse pas de requête HTTP
  • Le domaine ne log pas (sauf eventuellement du logging de debug tres basique)
  • Le domaine ne gere pas l'authentification

Le domaine exprime le QUOI. Les adaptateurs gerent le COMMENT. C'est la séparation des responsabilités au sens le plus strict.

Sur paltemps.fr, quand on fait des revues de code, la première chose qu'on vérifié c'est les imports du dossier domain/. Un import technique dans le domaine, c'est un signal d'alarme immediat.


Résumé

  • Le domaine contient uniquement la logique métier : entités, value objects, fonctions pures
  • Aucune dépendance externe (pas d'ORM, pas de framework, pas de lib HTTP)
  • Les regles métier et transitions d'état vivent dans les entités
  • Le domaine exprime ce qui doit se passer, jamais comment

Article précédent : 01 - Le concept

Article suivant : 03 - Les ports : les interfaces du domaine

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.