Guide technique

Scaler des Systèmes Backend Sans Suringénierie : La Checklist Architecture YAGNI

Quand scaler et quand ne pas le faire. Monolithe + queue comme architecture la plus sous-estimée, vrais déclencheurs de scaling, goulets d'étranglement base de données, stratégie de cache et checklist YAGNI.

11 février 202614 min de lectureÉquipe d'Ingénierie Oronts

L'épidémie de suringénierie

On l'a vu trop souvent : une équipe de 3 ingénieurs qui construit une architecture microservices pour une application avec 200 utilisateurs. Cluster Kubernetes, service mesh, bus d'événements, API gateway, 5 bases de données distinctes et une stack de monitoring plus complexe que l'application elle-même. Six mois plus tard, ils déboguent des problèmes de systèmes distribués au lieu de construire des fonctionnalités.

L'inverse se produit aussi : un monolithe qui sert 50 000 utilisateurs simultanés, où chaque requête prend 5 secondes parce que la base de données est le goulet d'étranglement et qu'il n'y a pas de couche de cache. L'équipe ajoute des serveurs applicatifs, mais la base de données est le plafond.

Les deux sont des échecs architecturaux. Cet article couvre quand scaler, quoi scaler et comment éviter de scaler des choses qui n'en ont pas besoin. Pour des patterns de scaling spécifiques, consulte notre guide d'architecture système et notre guide d'architecture event-driven.

La checklist architecture YAGNI

Avant d'ajouter toute complexité architecturale (nouveau service, nouvelle base de données, nouveau message broker, nouvelle couche de cache), demande-toi :

QuestionSi OUISi NON
Est-ce qu'on a un problème de performance mesuré en ce moment ?Corrige le problème spécifiqueN'ajoute pas de complexité pour un problème hypothétique
Est-ce que ça résoudrait un problème qu'on a déjà rencontré ?À considérer, avec des preuvesNe résous pas des problèmes que tu n'as pas encore eus
Est-ce qu'on peut le résoudre avec une approche plus simple d'abord ?Utilise l'approche plus simpleLa solution complexe est peut-être nécessaire
Est-ce que 10x plus de trafic casserait l'architecture actuelle ?Planifie (mais ne construis pas encore)L'architecture actuelle est suffisante
Est-ce que l'équipe est assez grande pour maintenir ça ?Procède si au moins 2 personnes peuvent s'en occuperN'ajoute pas ce que personne ne peut maintenir
Est-ce qu'on peut revenir en arrière si ça ne marche pas ?Risque acceptableTrop risqué

La réponse par défaut à "est-ce qu'on devrait ajouter ça ?" est non. Ajoute de la complexité quand tu as des preuves que c'est nécessaire, pas quand tu imagines que ça pourrait l'être.

Monolithe + Queue : l'architecture la plus sous-estimée

Une application monolithique avec une queue de jobs en arrière-plan gère 90 % des charges SaaS. Un seul codebase, un seul déploiement, une seule base de données, et une queue pour le travail en arrière-plan.

┌─────────────────────────────────────────┐
│            Monolithe                     │
│                                          │
│  ┌──────────┐  ┌──────────┐            │
│  │  API      │  │  Admin   │            │
│  │  Routes   │  │  UI      │            │
│  └────┬─────┘  └────┬─────┘            │
│       │              │                   │
│  ┌────▼──────────────▼─────┐            │
│  │    Services              │            │
│  │    (logique métier)      │            │
│  └────┬────────────────────┘            │
│       │                                  │
│  ┌────▼──────────────────────┐          │
│  │  Base de données (PostgreSQL) │      │
│  └───────────────────────────┘          │
│                                          │
│  ┌───────────────────────────┐          │
│  │  Job Queue (BullMQ/Redis) │          │
│  │  - Envoi d'emails         │          │
│  │  - Indexation recherche   │          │
│  │  - Génération de rapports │          │
│  │  - Import de données      │          │
│  └───────────────────────────┘          │
└─────────────────────────────────────────┘

Pourquoi ça marche

  • Un seul codebase : le refactoring est sûr. Le rechercher-remplacer fonctionne. Les types traversent toute l'application.
  • Un seul déploiement : déploie une fois. Pas de coordination de services. Pas de rollbacks distribués.
  • Base de données partagée : les jointures fonctionnent. Les transactions sont ACID. Pas de casse-tête d'eventual consistency.
  • Job queue : les travaux longs (emails, imports, indexation) ne bloquent pas le traitement des requêtes.
  • Débogage simple : les stack traces sont complètes. Les logs sont au même endroit. Pas besoin de tracing distribué.

Quand Monolithe + Queue ne suffit plus

SymptômeCause racineSolution
Temps de réponse API > 2sRequêtes base de données trop lentesAjouter des index, optimiser les requêtes, read replicas
CPU base de données > 80 % soutenuVolume ou complexité des requêtes trop élevéRead replicas, couche de cache, optimisation des requêtes
Les déploiements font peurCodebase trop gros, tests trop lentsMeilleurs tests, feature flags, déploiements canary (PAS de microservices)
Une fonctionnalité casse toutPas de frontières entre modulesMeilleure organisation du code (PAS de microservices)
L'équipe ne peut pas travailler en parallèleConflits de code, enfer des mergesPropriété des modules, trunk-based development

Remarque : aucun de ces symptômes n'est résolu en ajoutant des microservices. Ils sont résolus par une meilleure ingénierie au sein du monolithe. Les microservices résolvent un problème différent.

Quand séparer : les vrais déclencheurs

Sépare en services distincts uniquement quand :

1. La taille de l'équipe l'impose. Quand tu as 20+ ingénieurs et que les frontières d'équipe ne correspondent pas au codebase. L'équipe A possède la fonctionnalité X et l'équipe B la fonctionnalité Y, et elles doivent déployer indépendamment. C'est la raison principale des microservices.

2. Des besoins de scaling différents. Le service de recherche nécessite 10x le compute du service de catalogue produits. Les faire tourner dans le même processus gaspille des ressources. Des services séparés peuvent scaler indépendamment.

3. Des besoins technologiques différents. Le service d'inférence ML a besoin de Python et de GPU. Le service API a besoin de Node.js. Les faire tourner dans un seul processus n'est pas pratique.

4. L'isolation des pannes. Un crash du service de paiement ne devrait pas planter le service de catalogue produits. Quand l'isolation des pannes est critique pour la continuité métier, des processus séparés aident.

5. Les frontières de conformité. Le traitement des paiements conforme PCI doit être séparé du reste de l'application pour des raisons d'audit. Un service séparé avec sa propre frontière de sécurité simplifie la conformité.

Ce qui N'EST PAS une raison de séparer

  • "Les microservices sont une bonne pratique" (c'est un compromis, pas une bonne pratique)
  • "On pourrait avoir besoin de scaler" (scale quand tu en as besoin, pas avant)
  • "Architecture propre" (les frontières de modules au sein d'un monolithe sont plus propres que les frontières de services)
  • "Développement motivé par le CV" (Kubernetes sur ton CV n'aide pas tes utilisateurs)

La base de données est généralement le goulet d'étranglement

Dans 80 % des problèmes de performance, la base de données est le goulet d'étranglement. Pas le code applicatif. Pas le réseau. La base de données.

Diagnostic

-- Trouver les requêtes lentes (PostgreSQL)
SELECT query, calls, mean_exec_time, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;

-- Trouver les index manquants
SELECT schemaname, tablename, seq_scan, idx_scan,
       CASE WHEN seq_scan > 0 THEN round(100.0 * idx_scan / (seq_scan + idx_scan), 1) ELSE 100 END AS idx_pct
FROM pg_stat_user_tables
WHERE seq_scan > 100
ORDER BY seq_scan DESC
LIMIT 20;

-- Trouver le bloat des tables
SELECT tablename, pg_size_pretty(pg_total_relation_size(tablename::regclass)) AS total_size,
       pg_size_pretty(pg_relation_size(tablename::regclass)) AS data_size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(tablename::regclass) DESC
LIMIT 20;

Ordre de correction

  1. Ajouter les index manquants (quelques minutes à implémenter, impact massif)
  2. Optimiser les requêtes lentes (réécrire les requêtes N+1, ajouter des clauses WHERE, utiliser EXPLAIN ANALYZE)
  3. Ajouter du cache (Redis pour les données fréquemment lues, rarement modifiées)
  4. Read replicas (router les requêtes de lecture vers les replicas, les écritures vers le primaire)
  5. Connection pooling (PgBouncer entre l'application et la base de données)
  6. Scaling vertical (plus grosse instance, plus de RAM, stockage plus rapide)
  7. Scaling horizontal (sharding, uniquement quand tout le reste est épuisé)

La plupart des équipes sautent directement à l'étape 7 sans essayer les étapes 1-3. Les étapes 1-3 sont gratuites ou peu coûteuses et résolvent souvent le problème entièrement.

Stratégie de cache

Quoi mettre en cache

DonnéesCache ?TTLInvalidation
Sessions utilisateurOuiDurée de la sessionÀ la déconnexion
Catalogue produitsOui5-15 minutesSur événement de mise à jour produit
Résultats de recherchePeut-être1-5 minutesSur mise à jour de l'index
Données spécifiques à l'utilisateurAvec prudenceCourt (1-5 min)Sur action de l'utilisateur
Config / paramètresOuiLong (1 heure)Sur changement admin
Agrégats calculésOui15-60 minutesSur changement des données sous-jacentes
Données temps réel (stock)NonN/AToujours récupérer en direct

Quoi NE PAS mettre en cache

  • Données utilisateur avec implications de confidentialité : un contenu de panier en cache servi au mauvais utilisateur est une fuite de données.
  • Données qui changent rapidement : niveaux de stock, prix en direct, enchères. Le TTL du cache ne peut pas suivre.
  • Opérations lourdes en écriture : si les données changent plus souvent qu'elles ne sont lues, le cache ajoute de la complexité sans bénéfice.

Pattern d'invalidation de cache

// Invalidation pilotée par événement (recommandé)
eventBus.on('product.updated', async (product) => {
    await cache.delete(`product:${product.id}`);
    await cache.delete(`product-list:${product.categoryId}`);
    // Ne pas essayer de mettre à jour le cache ici. Laisser la prochaine lecture le remplir.
});

// Read-through avec stale-while-revalidate
async function getProduct(id: string): Promise<Product> {
    const cached = await cache.get(`product:${id}`);
    if (cached && !isExpired(cached)) {
        return cached.data;
    }

    // Périmé mais on le sert pendant le rafraîchissement
    if (cached && isExpired(cached)) {
        refreshInBackground(id); // async, pas d'await
        return cached.data;      // servir le périmé
    }

    // Cache miss
    const product = await db.products.findById(id);
    await cache.set(`product:${id}`, product, { ttl: 300 });
    return product;
}

La conversation scaling avec les parties prenantes

Les ingénieurs veulent scaler pour des raisons techniques. Les parties prenantes veulent scaler pour des raisons business. La conversation a besoin d'un langage commun.

La partie prenante ditCe qu'elle veut direRéponse de l'ingénierie
"On doit gérer 10x le trafic"Campagne marketing à venir, ou voeu pieux"Quel est le calendrier prévu ? Testons d'abord la capacité actuelle."
"Le site est lent"Une page est lente, pas tout le site"Quelle page ? Laisse-moi la profiler."
"On a besoin de microservices"Ils ont lu un article ou entendu ça en conférence"Quel problème on résout ? Laisse-moi te montrer l'architecture actuelle."
"Est-ce qu'on peut gérer le Black Friday ?"Préoccupation légitime sur les pics de trafic"Testons à 3x le pic actuel et voyons où ça casse."

Commence toujours par mesurer. "Le site est lent" n'est pas un besoin de scaling. "La page de listing produits prend 4 secondes à 500 utilisateurs simultanés, l'objectif est 500 ms" est un besoin de scaling avec un objectif mesurable.

Erreurs courantes

  1. Microservices à 100 utilisateurs. Si ton monolithe gère la charge et que ton équipe fait moins de 10 personnes, les microservices ajoutent de la complexité opérationnelle sans bénéfice.

  2. Cache sans stratégie d'invalidation. Ajouter un cache est facile. Le garder cohérent avec la base de données est difficile. Planifie l'invalidation avant d'ajouter le cache.

  3. Scaler l'application quand la base de données est le goulet d'étranglement. Ajouter 5 serveurs applicatifs supplémentaires n'aide pas si tous attendent la même requête lente en base de données.

  4. Pas de tests de charge. "On pense qu'on a besoin de scaler" n'est pas une preuve. Teste à 3-5x le pic de trafic actuel. Regarde où ça casse vraiment.

  5. Le tabou du scaling vertical. Une plus grosse instance de base de données coûte 200 $/mois de plus et prend 10 minutes à provisionner. C'est presque toujours moins cher qu'une semaine de travail architectural.

  6. Sharding avant d'avoir tout essayé. Le sharding de base de données est la solution de scaling la plus complexe. Essaie les index, l'optimisation de requêtes, le cache, les read replicas et le scaling vertical d'abord.

  7. Construire pour un scale que tu n'atteindras jamais. Si ton SaaS a 500 utilisateurs et croît de 10 % par mois, tu n'as pas besoin de gérer 1 million d'utilisateurs. Tu as besoin de gérer 5 000 utilisateurs l'an prochain.

Points clés à retenir

  • Monolithe + queue gère 90 % des charges SaaS. Un seul codebase, une seule base de données, un seul déploiement, et une queue de jobs pour le travail en arrière-plan. Ne sépare pas avant d'avoir des preuves que c'est nécessaire.

  • La base de données est généralement le goulet d'étranglement. Ajoute des index, optimise les requêtes, ajoute du cache, puis considère les read replicas. La plupart des problèmes de performance se résolvent aux étapes 1-3.

  • Sépare les services pour la taille de l'équipe, pas le scaling technique. Les microservices résolvent le problème "20 ingénieurs ne peuvent pas travailler dans un seul codebase", pas le problème "on a besoin de plus de performance".

  • Mesure avant de scaler. Teste à 3-5x le pic actuel. Profile les pages lentes. Trouve le vrai goulet d'étranglement. Puis corrige ce point précis.

  • Cache les lectures, pas les écritures. Mets en cache les données lues fréquemment et qui changent rarement. Invalide sur les événements de changement. Sers des données périmées pendant le rafraîchissement en arrière-plan.

  • Scale de la façon la moins chère d'abord. Une plus grosse instance de base de données (quelques minutes, 200 $/mois) vaut mieux qu'un mois de travail architectural. Le scaling vertical n'est pas un échec.

Nous aidons les équipes à scaler leurs systèmes de manière appropriée dans le cadre de notre pratique de logiciels sur mesure et de consulting. Si tu as besoin d'aide pour des décisions de performance ou de scaling, parle à notre équipe ou demande un devis.

Sujets couverts

architecture scalingscaling backendquand scaleroptimisation prématuréearchitecture YAGNImonolithe vs microservicesstratégie cachegoulet base de données

Prêt à construire des systèmes IA prêts pour la production ?

Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.

Démarrer une conversation