Fil rouge : une plateforme e-commerce à modules isolés (type Medusa 2.0), une migration de colonne à déployer sans coupure, un montant de 899,99 € à stocker, et un RAG à N = 10⁷ embeddings de dimension 1 536 qu'il faut interroger en moins de 100 ms.
Données — contraintes, migrations, argent
QInvariant garanti par une contrainte de base ou par le chemin d'écriture applicatif : quelle différence ?›
La contrainte s'applique à toute écriture, y compris celles qui n'existent pas encore — un script, un autre service, un dev futur. Le chemin applicatif ne protège que les écritures qui passent par lui. Corollaire : une garantie absente et connue se compense (test, note, condition de réouverture).
« Une contrainte vit avec les données, donc elle survit à tout nouveau code ; une validation vit dans un chemin, donc elle meurt au premier contournement. »
QUn ORM fait du soft delete (deleted_at). Quelle conséquence sur une contrainte d'unicité ?›
L'unicité doit devenir un index partiel, restreint aux lignes vivantes (WHERE deleted_at IS NULL). Sinon il devient impossible de recréer un enregistrement avec un e-mail déjà utilisé puis supprimé.
« Les lignes supprimées restent dans la table, donc elles participent à l'unicité, donc il faut exclure les lignes mortes de l'index. »
QUne migration commitée et déjà appliquée ailleurs est incorrecte. Que faire ?›
Ajouter une nouvelle migration corrective ; ne jamais modifier une migration déjà appliquée : les environnements qui l'ont jouée ne la rejoueront pas, et leur schéma divergerait du code.
« Une migration appliquée est un fait dans chaque base, donc la réécrire crée deux histoires, donc on avance par une nouvelle migration. »
QRenommer une colonne sans interruption, migration et déploiement non atomiques : comment ?›
Expand / contract, en déploiements séparés : 1. Expand — ajouter la nouvelle colonne, nullable, écrire dans les deux. 2. Backfill. 3. Basculer les lectures. 4. Contract — supprimer l'ancienne. Chaque étape est déployable et réversible, et aucune ne casse la version précédente du code.
« Pendant le déploiement deux versions du code coexistent, donc le schéma doit satisfaire les deux, donc on ajoute avant de retirer et on retire seulement quand plus rien ne lit l'ancien. »
QPourquoi jamais un montant dans un flottant, et qu'est-ce qu'un test de relecture ne prouve pas ?›
0,1 est périodique en binaire : 0,1 + 0,2 ≠ 0,3, et les sommes dérivent. Stocker en centimes entiers. Un test qui écrit 899.99 via JSON, relit et compare ne distingue pas un stockage exact d'un stockage flottant : la perte a lieu une fois à la conversion du littéral, identiquement des deux côtés — il prouve la fidélité du stockage, pas l'exactitude de la représentation.
« La fraction décimale n'est pas finie en base 2, donc chaque montant porte une erreur, donc on stocke des entiers ; et un aller-retour ne voit pas une erreur commise avant lui, donc il ne teste pas la représentation. »
Frontières — modules, transactions, DSL, flags
QArchitecture à modules isolés : pourquoi interdire une clé étrangère entre deux modules ?›
Pour qu'un module reste remplaçable : ses données pourraient un jour ne plus vivre dans la même base (autre schéma, autre service, autre techno). Une FK inter-modules soude les deux au même moteur. Les liens se font par identifiants et via l'API du module. Une FK reste possible à l'intérieur d'un module.
« Une FK est une promesse du moteur de base, donc elle suppose une base commune, donc elle interdit de sortir un module, donc on la remplace par un lien logique. »
QUne opération enchaîne des écritures dont certaines hors base (fichier S3, API tierce). Que garantit une transaction ?›
Une transaction ne sait annuler que les écritures de sa propre base : ni supprimer un fichier ni annuler un appel HTTP. Il faut des étapes de compensation explicites (motif saga). Prix : l'état n'est jamais atomique, seulement éventuellement cohérent.
« Le rollback n'atteint que ce que la base contrôle, donc chaque effet externe doit avoir son geste inverse, donc on orchestre une saga. »
QDans un DSL qui construit un graphe d'exécution (workflow, DAG, TF 1.x), pourquoi les if et for ordinaires ne font-ils pas ce qu'on croit ?›
La fonction s'exécute une seule fois, au chargement, pour construire le graphe — pas à chaque exécution. Un if y choisit une branche pour toujours, un for déroule ses itérations dans le graphe. Le contournement est une primitive qui diffère le calcul (transform() en Medusa, tf.cond).
« Le code Python décrit le graphe et ne l'exécute pas, donc toute condition évaluée en Python est figée à la construction, donc il faut une primitive du DSL pour brancher au runtime. »
QUne feature derrière un flag touche vingt endroits. Comment éviter vingt if (flag) ?›
Branch by abstraction : extraire une interface, écrire deux implémentations qui la respectent, brancher une fois à l'endroit de résolution. Retirer le flag = effacer la fonction de résolution et une implémentation, au lieu de traquer vingt conditions.
« Vingt conditions dispersées sont vingt points de dérive, donc on concentre le choix en un point derrière une interface, donc le retrait du flag devient une suppression locale. »
Livraison — trunk-based, rebase, tests
QTrunk-based development : quel paramètre détermine le coût de l'intégration ?›
La durée de vie des branches — ni le nombre de développeurs, ni la taille du changement. Le coût d'un merge croît avec ce qui a divergé pendant la vie de la branche. D'où la règle des 24-48 h. Corollaire contre-intuitif : plus il y a de parallélisme, plus il faut intégrer souvent.
« La divergence s'accumule avec le temps, donc plus la branche vit longtemps plus le merge coûte, donc on raccourcit les branches plutôt que les changements. »
QAprès un rebase, git status -sb affiche [ahead 8, behind 8]. Que s'est-il passé, que faire ?›
Le même nombre des deux côtés signe une réécriture d'historique, pas un travail divergent : le rebase a recréé les 8 commits avec de nouveaux hashes, la branche distante a les anciens. Faire git push --force-with-lease. Surtout pas git pull : en merge il réintègre les vieux commits en double, en rebase il les rejoue.
« Un rebase change les identités des commits, donc le remote et le local ont deux versions des mêmes changements, donc on écrase le remote avec un force protégé. »
QUn test vérifie qu'une route renvoie la bonne ressource par id, avec une seule ligne en base. Que prouve-t-il ?›
Rien : avec une seule ligne, « renvoyer la première ligne » passe aussi, comme un filtre partiel puis recherche en mémoire. Un test ne démontre une sélection que si le jeu contient des alternatives plausibles que le bug choisirait.
« Un test discrimine entre implémentations, donc il faut des données où la mauvaise implémentation donne un autre résultat, donc au moins deux lignes dont une piège. »
Estimation à l'échelle — RAG, IVF, FLOP
QRAG, N = 10⁷ embeddings de dimension 1 536, top-10 pour une requête : coût de la recherche exacte ?›
Brute force : distance de q à chacun des N vecteurs, N·d = 1,5·10¹⁰ multiplications-additions, puis un top-k en O(N log k). Sur un CPU à 10¹⁰ FLOP/s : de l'ordre de la seconde par requête — inacceptable à 100 ms. Règle de composition : hors boucle → « + », dans la boucle → « × ».
« Chaque vecteur coûte d opérations et il y en a N, donc N·d par requête, donc à 10⁷ × 1 536 on est à 10¹⁰ opérations, donc il faut un index. »
QComment un index IVF évite-t-il le scan complet ?›
Hors ligne : k-means sur les N vecteurs, chacun rangé dans la cellule de son centroïde. En ligne : comparer q aux k centroïdes, puis scanner les nprobe cellules les plus proches (≈ N/k vecteurs chacune). Coût ≈ k + nprobe·N/k (fois d). Deux temps séquentiels, donc les coûts s'additionnent. Approché : le vrai voisin peut être dans une cellule voisine.
« On paie d'abord la comparaison aux centroïdes, puis le scan d'une fraction, donc k + N/k, donc on troque un peu de rappel contre un facteur √N de vitesse. »
QIVF, nprobe = 1, coût k + N/k : quel k, et pourquoi ?›
Les deux termes varient en sens inverse, donc l'optimum les égalise, donc k = √N. Coût 2√N ≈ 6 300 vecteurs au lieu de 10⁷. Dérivation : f'(k) = 1 − N/k² = 0. L'argument d'équilibrage vaut pour tout coût en a·k + b/k.
« Un terme croît en k et l'autre décroît en 1/k, donc le minimum est atteint quand ils sont égaux, donc k = √N. »
QTu as estimé un nombre de FLOP. Comment passer au temps, et quelle confusion coûte cher au tableau ?›
Diviser un travail par un débit : temps = FLOP / (FLOP/s de la machine). FLOP est une quantité, FLOP/s une vitesse — les confondre donne des temps absurdes. Ordres de grandeur : CPU 10¹⁰-10¹¹ FLOP/s, GPU 10¹³-10¹⁴.
« Un travail se mesure en opérations et une machine en opérations par seconde, donc leur quotient est un temps, donc on vérifie les unités avant le chiffre. »