3 décisions d'architecture qui ont sauvé mes projets (et celle qui en a tué un)
Faire un choix d'archi en 2 minutes pendant un café peut éviter 6 mois de refacto — ou les déclencher. Quatre vraies décisions, prises sur mes projets en production, avec le coût et le résultat. Sans bullshit, sans 'best practices', juste ce qui marche et ce qui ne marche pas.
Pourquoi cet article
Un développeur senior n'est pas quelqu'un qui sait comment faire les choses. C'est quelqu'un qui sait quoi faire — et surtout quoi ne pas faire.
Je vais raconter 4 décisions d'architecture que j'ai prises sur mes projets en production ces 18 derniers mois. Trois ont rapporté gros. Une m'a coûté 6 semaines.
Les quatre m'ont appris ce que les certifications ne peuvent pas enseigner.
Décision 1 : "Supprimer tous les endpoints REST"
Projet : OneRP (2 048 joueurs concurrents en temps réel) Quand : Janvier 2025 Coût initial : 2 jours de réécriture du squelette Économie totale : ~8 semaines sur 12 mois
Le contexte
OneRP devait synchroniser des données entre un serveur, un client de jeu, et 48 mini-applications React (téléphone in-game, banque, police, etc.). En architecture classique, ça donnerait :
- Un endpoint REST par opération (
GET /api/player,POST /api/banking/transfer, …) - Probablement 200+ endpoints au total.
- Du WebSocket pour le push.
- Chaque écran React qui poll ou s'abonne aux bons events.
C'est ce que j'avais commencé à faire pendant 3 jours. Et c'était l'enfer.
Chaque nouvelle feature demandait :
- Créer la route serveur.
- Définir le DTO de requête + réponse.
- Écrire le client TypeScript.
- Gérer le cache côté client.
- Décider quoi invalider quand quelque chose change.
- Sur quel event WebSocket s'abonner pour le push.
Six étapes pour ajouter une feature. Chacune source de bugs.
La décision
J'ai supprimé tout ça. Plus de routes REST. Plus de DTOs. Plus de cache à invalider.
À la place : un seul canal binaire WebSocket. Chaque "lecture" est une méthode C# sur le serveur :
[ComputeMethod]
public virtual async Task<PlayerInfo> GetPlayer(string id, CancellationToken ct)
=> await _db.Players.FirstAsync(p => p.Id == id, ct);
Le framework (ActualLab Fusion) génère le client TypeScript, gère le cache, et invalide automatiquement tous les écrans qui dépendent de cette donnée quand elle change.
Le résultat
- 0 endpoint REST dans tout le projet.
- 111 ComputeServices au lieu de 200+ routes.
- 0 ligne de code de cache côté client.
- 0 ligne de code d'invalidation manuelle.
- Latence p99 : 0,8 ms (sub-milliseconde).
J'ai répliqué exactement cette archi sur 3 autres SaaS depuis. À chaque fois, 3 semaines de gagnées sur le boilerplate.
La leçon
Le bon choix d'archi se reconnaît à ceci : plus le projet grandit, moins il y a de code à écrire pour ajouter une feature. Si ton archi te fait payer un coût linéaire par feature, c'est qu'il manque une abstraction quelque part.
Décision 2 : "Faire concourir 9 algorithmes au lieu d'en choisir un"
Projet : SaleCast (forecasting e-commerce multi-marketplaces) Quand : Septembre 2025 Coût initial : 1 semaine pour écrire le moteur de compétition Bénéfice : précision +47 % vs le meilleur algo unique
Le contexte
SaleCast doit prédire les ventes par produit pour 2 800 SKU. Question évidente : quel algorithme de forecasting utiliser ?
J'ai posé la question à 4 data scientists. Quatre réponses différentes :
- "Holt-Winters, classique et solide."
- "LightGBM, gradient boosting bat tout."
- "Prophet, c'est ce que Facebook utilise."
- "TiRex, le nouveau foundation model d'NX-AI."
Personne n'avait raison. Et personne n'avait tort.
Parce qu'aucun algorithme ne gagne sur 100 % des produits. Holt-Winters est bon pour le t-shirt qui se vend tous les jours, catastrophique pour la guirlande de Noël. Croston est conçu pour le produit en déclin lent, ridicule sur le saisonnier.
La décision
Au lieu de choisir, j'ai écrit un moteur de compétition. Chaque nuit, pour chaque produit, mes 9 algorithmes statistiques + 2 foundation models (TiRex, Chronos) s'affrontent sur 10 fenêtres de back-test glissantes. Le gagnant remplace l'ancien.
Le code est moins beau que "je sais que Holt-Winters est le bon choix". Mais c'est mesurablement plus précis.
Le résultat
Sur 2 800 SKU, le gagnant varie :
- TiRex gagne sur 38 % des produits (foundation model)
- LightGBM gagne sur 22 % (ML.NET)
- HoltWinters gagne sur 18 %
- Croston gagne sur 14 % (intermittent)
- 6 autres se partagent le reste.
Le baseline Naive gagne sur 0 % des produits. Si Naive gagne, c'est qu'on a un bug — pas un produit.
L'erreur moyenne (MAE) du système est 47 % plus basse que si j'avais choisi un seul algorithme.
La leçon
Quand 4 experts ont 4 opinions, c'est rarement parce qu'ils sont incompétents. C'est parce que la bonne réponse dépend du contexte que personne n'a regardé.
Plutôt que de choisir un expert, fais-les concourir sur les vraies données. C'est plus humble, plus précis, et ça marche mieux en production.
Décision 3 : "Faire fonctionner l'app sans réseau, comme priorité 1"
Projet : MyRoadTrip (planificateur de voyage collaboratif) Quand : Mars 2025 Coût initial : 2 semaines pour intégrer Yjs (CRDT) Bénéfice : 0 perte de donnée signalée en production
Le contexte
MyRoadTrip est une app de planification de road trip. Le cas d'usage est précis : deux personnes éditent le même itinéraire dans une voiture, à travers les Alpes, sans réseau pendant 3 heures.
L'approche classique :
- Stocker les modifications en local quand offline.
- Au retour du réseau, envoyer la file d'attente.
- Si conflit, demander à l'utilisateur de choisir quelle version garder.
C'est ce que font 95 % des apps. Et c'est exactement ce qu'aucun utilisateur ne veut.
"On a roulé 3 heures, j'ai changé 5 étapes, et l'app me demande maintenant quelle version je veux garder. Mais l'autre conducteur dort. Et je sais plus ce que j'ai changé."
La décision
Pas de file d'attente. Pas de conflit manuel. Pas de notion de "version qui gagne".
À la place : un CRDT (Conflict-free Replicated Data Type) au niveau du champ. Si Alice change le nom de l'étape, et Bob change la durée pendant ce temps, les deux changements passent. Pas l'un ou l'autre. Les deux.
C'est mathématiquement garanti par la structure de données. Pas par un algorithme intelligent.
Le résultat
- 0 conflit signalé par les utilisateurs en 12 mois de prod.
- Compression de 98 % sur les paquets de sync (on envoie les deltas, pas le doc).
- Polling adaptatif : 2s en édition active, 60s sur batterie faible.
J'ai extrait le pattern en bibliothèque réutilisable (@portfolio/crdt-engine).
Je l'ai réutilisé sur 2 autres projets depuis. Coût marginal par projet : 0,5 jour.
La leçon
Quand un user fait face à un conflit, il a déjà perdu. Le bon design ne fait pas "gérer mieux les conflits". Le bon design rend les conflits mathématiquement impossibles.
CRDT, idempotence, additivité, monotonie — la liste des outils existe. Trop peu d'apps les utilisent.
Décision 4 : Celle qui m'a coûté 6 semaines
Projet : GeopolAI (war room IA géopolitique) Quand : Août 2025 Coût : 6 semaines à refaire l'archi
Le contexte
GeopolAI compare les réponses de 3 IA (GPT-4, Claude, Gemini) sur des scénarios géopolitiques et mesure leurs biais. Au mois 1, j'avais besoin de visualiser une carte du monde animée avec des marqueurs qui apparaissent quand l'IA mentionne un pays.
La mauvaise décision
J'ai choisi D3.js + react-simple-maps + canvas overlays parce que "tout le monde utilise D3 pour les data viz géo".
C'était vrai en 2018. Pas en 2025.
Pourquoi c'était une mauvaise décision
- D3 et React combattent constamment sur qui contrôle le DOM.
- Les animations cartographiques se font dans 3 couches incompatibles (SVG D3 + Canvas + React).
- Le re-render d'un seul marqueur déclenche un re-layout de toute la carte.
- À 12 marqueurs animés en simultané, la page tombait à 15 FPS sur un MacBook M2.
J'ai passé 4 semaines à essayer de réconcilier D3 et React. Hooks pour synchroniser les refs, gestion manuelle du lifecycle, memoization agressive.
Au bout de 4 semaines, j'avais une carte qui marchait. À 22 FPS. Et que je détestais maintenir.
Le pivot
J'ai jeté. 4 semaines de code, à la corbeille.
J'ai recommencé avec un canvas pur + mes propres primitives géographiques. Une seule couche de rendu. Une seule API de mise à jour. Un seul thread de rendu.
2 semaines plus tard, j'avais une carte qui tournait à 60 FPS avec 50 marqueurs animés. Plus simple, plus rapide, plus maintenable.
La leçon
Le réflexe "j'utilise ce que tout le monde utilise" est le piège principal du développeur intermédiaire.
Une lib populaire en 2018 peut être objectivement dépassée en 2025, même si elle est toujours populaire — surtout si elle a 50K stars sur GitHub. Les étoiles ne se déprécient pas avec le temps. Les pratiques, si.
Aujourd'hui, je passe systématiquement 1-2 heures à benchmarker une lib avant de la choisir sur un projet sérieux. Pas pour la performance brute — pour voir si elle se bagarre avec mon framework principal.
Si une lib se bat avec React, Blazor, ou Vue : je passe mon chemin, même si c'est la plus populaire du moment.
Cette habitude m'a fait gagner les 6 semaines que j'avais perdues sur GeopolAI.
Ce que ces 4 décisions ont en commun
Aucune n'était évidente sur le moment. Aucune n'est dans un livre d'architecture. Aucune n'est apparue dans une formation.
Toutes les quatre venaient de questions inconfortables que je me suis posé :
- "Pourquoi est-ce que j'écris autant de boilerplate ?" → suppression REST
- "Pourquoi devrais-je faire confiance à un expert quand il y a 4 experts contradictoires ?" → compétition d'algos
- "Qu'est-ce qui se passe vraiment quand l'utilisateur perd le réseau ?" → CRDT
- "Pourquoi est-ce que je me bats avec ma lib depuis 3 semaines ?" → pivot Canvas
Le développeur senior n'a pas plus de réponses que le junior. Il pose juste des questions plus inconfortables — et il s'autorise à y répondre, même quand la réponse va à l'encontre de ce que "tout le monde fait".
C'est, en une phrase, ce qui sépare les deux.
Florian Sola
Lead Technique · Haute performance temps réel · 9 ans d'expérience