Concept #022
ArchitectureFail Fast
- architecture
- bonnes pratiques
- résilience
Le principe en une phrase
Fail Fast signifie qu'un systeme doit signaler immédiatement et bruyamment toute condition d'erreur, plutot que de tenter de continuer dans un état dégradé ou incohérent.
C'est douloureux sur le moment, mais salvateur pour la dette technique à long terme. Un crash explicite au moment exact du problème est infiniment plus facile à diagnostiquer qu'un comportement bizarre découvert trois semaines plus tard en production, à trois couches de distance de la cause réelle.
Le problème : les erreurs silencieuses
Imaginez une fonction qui reçoit un paramètre invalide. Deux approches possibles :
L'approche "tolérante" (Fail Slow) :
function calculateDiscount(price: number, percentage: number): number {
if (percentage < 0 || percentage > 100) {
return 0; // on retourne une valeur par défaut "sûre"
}
return price * (percentage / 100);
}Ce code ne plante pas. Il retourne 0 silencieusement quand les paramètres sont invalides. Le problème ? Le code appelant ne sait pas qu'il y a eu un souci. La réduction de 0 sera appliquée, le client paiera plein pot, et personne ne saura pourquoi -- jusqu'à ce qu'un ticket de support arrive trois jours plus tard.
L'approche Fail Fast :
function calculateDiscount(price: number, percentage: number): number {
if (percentage < 0 || percentage > 100) {
throw new RangeError(
`Le pourcentage doit être entre 0 et 100, reçu : ${percentage}`
);
}
if (price < 0) {
throw new RangeError(
`Le prix ne peut pas être négatif, reçu : ${price}`
);
}
return price * (percentage / 100);
}Ce code explose immédiatement avec un message clair. L'erreur est détectée à la source, pas trois couches plus loin. Le développeur voit exactement quel paramètre est invalide et pourquoi.
Fail Fast à différents niveaux
Le principe s'applique à toutes les couches d'une application.
Validation des entrées
La première ligne de défense. Toute donnée qui entre dans le système doit être validée le plus tôt possible.
// Fail Fast : valider dès l'entrée dans le système
function createUser(input: unknown): User {
const parsed = UserSchema.safeParse(input);
if (!parsed.success) {
throw new ValidationError(
`Données utilisateur invalides : ${parsed.error.message}`
);
}
return new User(parsed.data);
}Avec une librairie comme Zod ou Joi, la validation est déclarative et les erreurs sont explicites. Ne laissez jamais des données invalides traverser votre système en espérant que "ça passera".
Construction d'objets
Le constructeur est le gardien de la cohérence d'un objet. Si un objet ne peut pas être créé dans un état valide, il ne doit pas être créé du tout.
class EmailAddress {
private readonly value: string;
constructor(email: string) {
if (!email || !email.includes("@")) {
throw new Error(`Adresse email invalide : "${email}"`);
}
this.value = email.toLowerCase().trim();
}
toString(): string {
return this.value;
}
}
// Impossible de créer un EmailAddress invalide
const email = new EmailAddress(""); // Error: Adresse email invalide : ""Ce pattern s'appelle un Value Object. L'objet garantit son propre invariant. Si vous avez un EmailAddress en main, vous savez qu'il est valide -- pas besoin de revérifier plus tard.
Configuration au démarrage
Une application doit vérifier sa configuration au démarrage, pas au premier appel qui en a besoin.
// Fail Fast au démarrage
function loadConfig(): AppConfig {
const requiredVars = [
"DATABASE_URL",
"JWT_SECRET",
"REDIS_HOST",
] as const;
const missing = requiredVars.filter((v) => !process.env[v]);
if (missing.length > 0) {
throw new Error(
`Variables d'environnement manquantes : ${missing.join(", ")}. ` +
`L'application ne peut pas démarrer.`
);
}
return {
databaseUrl: process.env.DATABASE_URL!,
jwtSecret: process.env.JWT_SECRET!,
redisHost: process.env.REDIS_HOST!,
};
}
// Appelé au tout début du main()
const config = loadConfig();Si une variable d'environnement manque, l'application refuse de démarrer. Mieux vaut un déploiement échoué et visible qu'un serveur qui tourne en apparence mais qui plante sur la première requête nécessitant la base de données.
Fail Fast dans les assertions
Les assertions sont l'outil le plus direct du Fail Fast. Elles expriment des conditions qui doivent être vraies à un point donné du programme.
function processPayment(order: Order): Receipt {
// Ces assertions documentent les pré-conditions
console.assert(order.items.length > 0, "Commande vide");
console.assert(order.total > 0, "Montant invalide");
console.assert(order.status === "confirmed", "Commande non confirmée");
// Si on arrive ici, on est sûr que l'état est cohérent
return chargeCustomer(order);
}En TypeScript/JavaScript, console.assert ne lance pas d'exception en production. Pour un vrai Fail Fast, utilisez une fonction assert qui lance :
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new AssertionError(message);
}
}Fail Fast vs Fail Safe : deux philosophies complémentaires
Il est important de distinguer Fail Fast et Fail Safe. Ce ne sont pas des opposés mais des stratégies complémentaires appliquées à des contextes différents.
Fail Fast : détecter et signaler l'erreur immédiatement. Utilisé en développement, dans la logique métier, et partout où un état incohérent est inacceptable.
Fail Safe : en cas d'erreur, se rabattre sur un état sûr. Utilisé aux frontières du système, dans les interactions avec l'utilisateur, et dans les systèmes critiques.
// Fail Fast dans le service métier (interne)
function debitAccount(accountId: string, amount: number): void {
const account = accountRepository.findById(accountId);
if (!account) {
throw new Error(`Compte ${accountId} introuvable`);
}
if (account.balance < amount) {
throw new InsufficientFundsError(account.balance, amount);
}
account.debit(amount);
}
// Fail Safe dans le contrôleur HTTP (frontière)
app.post("/api/transfers", async (req, res) => {
try {
await debitAccount(req.body.accountId, req.body.amount);
res.status(200).json({ success: true });
} catch (error) {
if (error instanceof InsufficientFundsError) {
res.status(422).json({ error: "Fonds insuffisants" });
} else {
// Logger l'erreur en détail, mais répondre proprement
logger.error("Erreur inattendue", { error });
res.status(500).json({ error: "Erreur interne" });
}
}
});La logique métier est Fail Fast : elle explose si quelque chose ne va pas. Le contrôleur HTTP, lui, attrape ces erreurs et les convertit en réponses HTTP propres. Les deux approches cohabitent dans la même application.
Les bénéfices concrets du Fail Fast
- Diagnostic rapide : l'erreur pointe directement vers la cause. Pas besoin de remonter une chaîne de conséquences.
- Moins de dette technique : les bugs sont corrigés à la source, pas contournés par des couches de code défensif.
- Code plus simple : après une validation Fail Fast, le reste de la fonction peut supposer que les données sont valides. Moins de vérifications redondantes.
- Confiance accrue : quand le code ne plante pas, vous savez que tout est cohérent -- pas juste que les erreurs sont masquées.
Résumé
Le Fail Fast est un acte de discipline. Il est tentant de retourner null, de mettre une valeur par défaut, ou d'ignorer un cas d'erreur "qui n'arrivera probablement jamais". Mais chaque erreur silencieuse est une bombe à retardement.
Un crash immédiat et bruyant vous force à corriger le bug à la source, avant qu'il ne soit enfoui sous d'autres fonctionnalités. La douleur est immédiate mais brève. L'alternative -- un bug silencieux qui corrompt des données pendant des semaines -- est bien plus coûteuse.