09 - La dette technique : quand on construit sans lifecycle
Ce que tu vas apprendre
- Ce qu'est vraiment la dette technique
- 7 problèmes concrets causes par l'absence de lifecycle
- Pour chaque problème, la solution "avec lifecycle"
Prerequisites
Qu'est-ce que la dette technique ?
La dette technique est l'ensemble des raccourcis pris pendant le développement qui coutent plus cher a maintenir qu'a avoir bien fait des le depart. C'est comme une dette financiere : tu empruntes du temps maintenant, mais tu paies des interets plus tard.
Ce n'est pas toujours mauvais : parfois, on prend de la dette technique consciemment pour livrer plus vite. Le problème, c'est quand elle est accidentelle (on ne savait pas qu'on en prenait) ou negligee (on sait mais on ne rembourse jamais).
L'absence de lifecycle est une des sources de dette technique les plus couteuses. Voici pourquoi, avec un cas d'etude réel.
Problème 1 : Pas de Single Source of Truth
Le constat
L'état d'une Place pendant un batch est disperse dans 4 systèmes :
| Système | Ce qu'il stocke | Problème |
|---|---|---|
| React state (RAM) | L'action en cours | Perdu au refresh |
| IndexedDB (navigateur) | Le statut de review | Local, pas partageable |
| Firestore | Les paramètres de config | Ne connaît pas l'état de la Place |
| PostgreSQL | La Place finale | Ne sait pas qu'un batch est en cours |
Avec un lifecycle
sql-- Un seul SELECT pour savoir ou en est chaque Place
SELECT place_id, status FROM places WHERE city_id = 42;
Un champ, une table, une vérité.
Problème 2 : Logique de categorisation ad-hoc
Le constat
Le code recalcule l'action a chaque exécution avec des if/else :
typescript// Machine a etats implicite dans du code imperatif
if (place.is_closed && exists) {
action = "deleted";
} else if (exists) {
action = "needs_review";
} else {
action = "pending_images";
}
C'est une state machine deguisee, mais illisible, non testable, et non documentee.
Avec un lifecycle
typescriptconst transitions = {
SCRAPED: {
CLOSE_DETECTED: { target: "PENDING_DELETE", guard: existsInDB },
DIFF_DETECTED: { target: "PENDING_REVIEW", guard: existsInDB },
NEW_PLACE: { target: "DRAFT", guard: notInDB },
},
};
Declaratif. Testable unitairement. Lisible par n'importe qui.
Problème 3 : Validation dupliquee
Le constat
Le meme guard est reimplemente dans plusieurs fichiers :
typescript// Fichier A : create-place-panel.tsx
const isValid =
hasImages && hasAllTops && hasTranslations && hasGptDescriptions && hasCategories && hasMappedData;
// Fichier B : add-place-search.tsx
// ... exactement le meme calcul, copie-colle
Si la regle change, il faut la changer partout. On en oublie un ? Bug.
Avec un lifecycle
typescript// Un guard unique, defini une fois, reutilise partout
function canTransitionTo_READY_FOR_IMAGES(place: PlaceEntity): boolean {
return (
place.categories.length > 0 &&
place.translations.every((t) => t.description !== null) &&
place.images.length > 0 &&
place.images.filter((i) => i.top !== null).length >= 3
);
}
Un seul endroit. Un seul test. Une seule source de vérité pour la regle.
Problème 4 : Duplication de features entières
Le constat
11 fichiers dupliques d'une feature vers une autre : place-type-dialog.tsx, image-scraping-panel.tsx, translations-panel.tsx... Meme logique, meme UI, copiee-collee.
Avec un lifecycle
Les features partagent le meme pipeline de domaine (les transitions). Chaque feature est un point d'entree qui déclenché des transitions, pas une copie du domaine. Pas de duplication, parce que la logique vit dans les transitions, pas dans les features.
Problème 5 : Pas de rollback ni d'idempotence
Le constat
Si le batch plante a la Place 500, il faut tout recommencer. Pas de moyen de savoir quelles Places ont ete traitees et lesquelles non.
Avec un lifecycle
Chaque Place a son état persiste. Si le batch plante, on relance : les Places deja traitees sont en ENRICHED, le batch les ignore (no-op idempotent) et reprend les DRAFT.
Problème 6 : Pas d'audit trail
Le constat
Personne ne sait qui a fait quoi, quand. Une Place est en PUBLISHED mais on ne sait pas qui l'a publiee, quand, ni depuis quel état elle venait.
Avec un lifecycle
Chaque transition est un événement auditable :
typescriptconst auditEntry = {
entityId: "place_42",
from: "READY_FOR_PUBLICATION",
to: "PUBLISHED",
triggeredBy: "user_123",
timestamp: "2026-03-28T14:32:00Z",
event: "PUBLISH",
};
Un historique complet, interrogeable, exportable.
Problème 7 : État et intention melanges dans les types
Le constat
Les types TypeScript melangent "ce que l'entité est" (son état) et "ce qu'on veut en faire" (l'intention) :
typescripttype BatchPlaceResult = {
place: Place;
action: "pending_images" | "needs_review" | "deleted"; // intention, pas etat
reviewStatus?: "approved" | "rejected"; // etat, mais local
};
action n'est pas un état de l'entité, c'est une intention du système. reviewStatus est un état, mais il n'existe que dans un objet local, pas dans la source de vérité.
Avec un lifecycle
typescripttype PlaceEntity = {
id: string;
status: PlaceStatus; // l'etat, persiste, autoritaire
// ... autres champs
};
type PlaceStatus =
| "DRAFT"
| "ENRICHED"
| "READY_FOR_IMAGES"
| "IMAGES_PROCESSING"
| "IMAGES_PROCESSED"
| "READY_FOR_PUBLICATION"
| "PUBLISHED";
L'état est dans l'entité. L'intention est dans la transition. Les deux ne se melangent pas.
Résumé
- La dette technique est le coût des raccourcis non rembourses
- L'absence de lifecycle produit 7 problèmes concrets : pas de SSOT, logique implicite, validation dupliquee, features copiees, pas d'idempotence, pas d'audit, types melanges
- Chaque problème a une solution directe quand on structure le code autour d'un lifecycle
- Le lifecycle n'est pas un luxe : c'est ce qui empeche la dette technique de s'accumuler
Article précédent : 08 - Idempotence Article suivant : 10 - Features vs Domaine : la bonne decoupe