Domaines et cycles de vie - 07 - Les invariants : ce qui doit toujours etre vrai

Les invariants en DDD : des conditions testables par état du cycle de vie. Comment les définir, les vérifier et les utiliser pour debugger en TypeScript.

07 - Les invariants : ce qui doit toujours etre vrai

Ce que tu vas apprendre

  • Ce qu'est un invariant et pourquoi c'est central
  • Les propriétés d'un bon invariant
  • Comment les invariants se lient aux états du cycle de vie
  • Comment tester les invariants

Prerequisites


Qu'est-ce qu'un invariant ?

Un invariant est une condition qui doit toujours etre vraie quand une entité est dans un état donne. C'est un contrat : "si tu me dis que cette Place est PUBLISHED, alors je te garantis que..."

Exemples :

  • "Une Place en état PUBLISHED doit avoir au moins 3 URL d'image valide"
  • "Une Place en état ENRICHED doit avoir au moins une categorie et une traduction avec description"
  • "Une Place en état IMAGES_PROCESSED doit avoir les Top 1, 2 et 3 assignes a trois images distinctes"

Un invariant n'est pas un souhait. C'est un fait verifiable. Si l'invariant est faux, le système est dans un état incoherent et il y a un bug.

Les propriétés d'un bon invariant

1. Testable

Tu dois pouvoir écrire un test automatise qui vérifié l'invariant. Si tu ne peux pas le tester, ce n'est pas un invariant, c'est un commentaire.

typescript// Invariant testable
function invariant_PUBLISHED(place: PlaceEntity): boolean {
  const validImages = place.images.filter((img) => img.url && img.url.startsWith("https://"));
  return validImages.length >= 3;
}

// Test
test("une Place PUBLISHED a au moins 3 images valides", () => {
  const place = buildPublishedPlace(); // factory de test
  expect(invariant_PUBLISHED(place)).toBe(true);
});

2. Binaire

Un invariant est vrai ou faux. Pas "a peu pres", pas "dans la plupart des cas". C'est un booleen.

typescript// Bon : binaire
const isValid = place.images.length >= 3; // true ou false

// Mauvais : pas binaire
const quality = place.images.length / 3; // 0.66... c'est quoi ? Valide ? Pas valide ?

3. Independent de l'implementation

L'invariant decrit une regle métier, pas un détail technique. Il ne doit pas dépendre de comment le code est écrit.

typescript// Bon : regle metier
"Une Place PUBLISHED a au moins 3 images valides"

// Mauvais : detail d'implementation
"Le tableau place.images a un length >= 3 et chaque element a une propriete url non null"

La première formulation survivra a un changement d'implementation (par exemple, si les images passent d'un tableau a une Map). La deuxieme, non.

Un invariant par état

Chaque état du cycle de vie devrait avoir ses invariants. Voici ceux du domaine Place :

État Invariant
DRAFT name non vide, googlePlaceId present
ENRICHED Au moins 1 categorie, au moins 1 traduction avec description
READY_FOR_IMAGES Toutes les traductions ont une description non vide
IMAGES_PROCESSING scrapedImages.length > 0
IMAGES_PROCESSED Au moins 3 images générées, Top 1/2/3 assignes
READY_FOR_PUBLICATION Images validees par un humain
PUBLISHED Au moins 3 URL d'image valide, toutes les traductions completes

Ces invariants sont cumulatifs : un invariant de PUBLISHED inclut implicitement tous les invariants des états précédents (puisqu'on ne peut arriver a PUBLISHED qu'en passant par tous les états d'avant).

Chaque bug est un invariant casse

C'est une regle puissante : tout bug peut etre decrit comme un invariant qui a ete viole.

  • "La Place est publiee mais elle n'a pas d'images" --> l'invariant de PUBLISHED est casse
  • "La Place est marquee ENRICHED mais n'a aucune categorie" --> l'invariant de ENRICHED est casse
  • "La Place est en IMAGES_PROCESSED mais les Tops ne sont pas assignes" --> l'invariant de IMAGES_PROCESSED est casse

Cette facon de penser transforme le debug : au lieu de chercher "ou est le bug dans le code", tu cherches "quel invariant a ete viole, et quelle transition l'a permis".

Comment tester les invariants

Verification a la transition

Le meilleur moment pour vérifier un invariant, c'est juste apres une transition. Si on vient de passer a ENRICHED, on vérifié l'invariant de ENRICHED :

typescriptfunction transition(place: PlaceEntity, event: string): PlaceEntity {
  // ... logique de transition ...
  const updated = { ...place, status: targetState };

  // Verifier l'invariant du nouvel etat
  const invariantCheck = invariants[targetState];
  if (invariantCheck && !invariantCheck(updated)) {
    throw new Error(`Invariant viole pour l'etat ${targetState}`);
  }

  return updated;
}

Verification periodique

Tu peux aussi vérifier les invariants de facon periodique (un cron job, par exemple) pour détecter les incoherences qui auraient echappe aux guards :

sql-- Trouver les Places PUBLISHED sans images
SELECT id, name FROM places
WHERE status = 'PUBLISHED'
AND id NOT IN (SELECT place_id FROM images GROUP BY place_id HAVING COUNT(*) >= 3);

Résumé

  • Un invariant est une condition qui doit toujours etre vraie dans un état donne
  • Il est testable, binaire, et independant de l'implementation
  • Chaque état du cycle de vie a ses propres invariants
  • Tout bug est un invariant viole : ca change ta facon de debugger
  • On vérifié les invariants a la transition et/ou periodiquement

Article précédent : 06 - Single Source of Truth (SSOT) Article suivant : 08 - Idempotence : exécuter sans crainte

Sources

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