Tous les articles

Série Ledger, partie I : le modèle en partie double

·
30 juin 2026
·
9 min de lecture
·
••• vues
·
••• j'aime

Traduit de l'article original en anglais. Lire en anglais

Série Ledger, partie I : le modèle en partie double

La plupart des systèmes financiers commencent de la même façon. Vous construisez un wallet, un utilisateur a $100, il envoie $30 à un ami, donc vous écrivez balance = balance - 30 sur une ligne et balance = balance + 30 sur l'autre. Le solde devient un simple nombre que vous gardez sur le wallet et que vous mettez à jour à chaque fois, comme le total d'une carte cadeau. Ça marche, et vous le poussez en prod comme ça.

Puis l'entreprise grandit, et vous avez maintenant des cartes de débit/crédit, des rails crypto, des versements en fiat, des frais, des remboursements, quelques prestataires de paiement, un fournisseur de wallet. Le système financier s'est éparpillé entre des services et des tables détenus par différentes équipes. Et un trimestre, la finance vous annonce que $40 000 ne sont rattachés à rien, impossible de les retrouver.

Techniquement, l'argent n'a pas disparu. Il est sur votre compte bancaire, chaque centime est bien là. Ce qui a disparu, c'est l'attribution ou la référence, la trace de à qui il appartient. Une colonne balance dans votre base de données contient un nombre mais pas toute l'histoire qui se cache derrière, donc quand le nombre et la réalité ne sont pas d'accord, il n'y a aucun moyen de rejouer la façon dont on en est arrivé là. Tous les ingénieurs qui ont travaillé sur un système financier critique ont leur propre version de cette histoire d'horreur, demandez-leur !

La solution n'est pas simplement une colonne balance mieux pensée. C'est un modèle que les banques et les systèmes financiers traditionnels utilisent depuis des années, et sur lequel tout ledger sérieux repose encore aujourd'hui : la partie double.

Pourquoi un seul nombre ne suffit pas

Stocker l'argent sous la forme d'un nombre modifiable, en partie simple, échoue pour trois raisons principales, et elles ont tendance à se manifester à 2 h 45 du matin, en pleine nuit.

Et vous ne pouvez rien retracer pour comprendre ce qui s'est passé, parce qu'il n'y a pas d'historique clair. balance = 70 ne dit rien des mouvements qui l'ont produit, c'est juste un nombre. Quand il est faux, il n'y a rien à auditer pour remonter le fil.

Il n'y a pas de contrepartie. Soustraire $30 d'un wallet n'enregistre pas où sont partis les $30. La source et la destination de l'argent n'existent que dans la ligne de code applicatif qui s'est exécutée à ce moment-là, pas dans les données centrales du système.

Et en plus, rien ne vous protège contre les race conditions qui vont ou peuvent se produire. Deux requêtes lisent balance = 100 au même moment, chacune soustrait 30, et chacune écrit 70. L'argent a bougé deux fois et la colonne aura quand même l'air correcte.

Les trois principes fondamentaux

Un ledger en partie double remplace ce nombre modifiable par un registre des mouvements en ajout seul, construit à partir de trois objets.

  1. Un compte : c'est simplement une réserve de valeur indépendante. Le wallet d'un utilisateur, la trésorerie de l'entreprise, les frais que vous avez collectés, chacun est un compte.

  2. Une écriture : c'est un seul débit ou un seul crédit sur un seul compte. Une écriture DOIT toujours être écrite une seule fois, sans jamais être modifiée ni supprimée. Chacune est une trace permanente de la valeur qui entre dans un compte ou qui en sort « immuable ».

  3. Une transaction : c'est un groupe d'écritures qui bougent d'un seul bloc, tout ou rien. C'est l'objet qui fait tenir tout votre système.

Vous ne créez pas les écritures directement. Vous écrivez une transaction, et une transaction porte ses écritures. L'enregistrement lui-même s'exécute dans une seule txn de base de données, donc c'est tout ou rien, tx, _ := db.Begin(); tx.Exec(...); tx.Commit(). Par exemple, un virement de $30 d'Alfred vers Tobi est une transaction qui contient deux écritures, une qui prend à Alfred et une qui donne à Tobi, et soit les deux sont commitées, soit aucune ne l'est. Il ne devrait jamais exister d'état valide où Alfred a été débité sans que Tobi ait été crédité. C'est ce regroupement atomique qui fait la garantie.

Débits et crédits

Maintenant, voici la partie qui embrouille pas mal de monde. Un débit n'est pas une soustraction et un crédit n'est pas une addition. Ce sont simplement les deux seules directions qu'un mouvement peut prendre, et le fait qu'une direction donnée augmente ↑ ou diminue ↓ un compte dépend du type de compte sur lequel elle s'applique. Pour plus de clarté, vous pouvez simplement relire la section « Les trois principes fondamentaux »

Chaque compte appartient à l'un des quelques types possibles dont nous avons parlé plus haut, et c'est le type qui fixe son solde normal, le côté qui fait monter le solde. Vous n'avez donc jamais affaire qu'à deux cas. D'abord, les comptes à solde normal débiteur, les actifs et les charges. Un débit les augmente, un crédit les diminue. Ensuite, les comptes à solde normal créditeur, les passifs, les capitaux propres et les produits. Un crédit les augmente, un débit les diminue.

La différence devient claire dès que vous l'appliquez à l'exemple d'un wallet. Les $100 qu'un utilisateur détient chez vous ne sont pas votre argent, c'est de l'argent que vous lui devez, donc dans vos livres c'est un passif, à solde normal créditeur. Les vrais dollars que vous détenez pour son compte sont un actif, à solde normal débiteur.

Et là, un dépôt prend tout son sens. Par exemple, un utilisateur dépose 100 USDC, et deux choses deviennent vraies en même temps : vous détenez 100 USDC de plus, un actif qui augmente, donc un débit, et vous devez 100 USDC de plus à l'utilisateur, un passif qui augmente, donc un crédit. Un seul mouvement, deux écritures, et elles s'équilibrent.

dépôt de 100 USDC en deux écritures

Débit et crédit ne sont pas des forces qui s'opposent. Ce sont deux vues des mêmes $100, enregistrées au même moment, dans une seule transaction.

Avec une analogie tirée de la physique, il y a une autre façon de voir et de comprendre tout cela, la célèbre troisième loi du mouvement de Newton « À toute action correspond une réaction égale et opposée »

Dans toute transaction, les débits et les crédits doivent être égaux. À chaque fois, sans aucune exception. Σ débits − Σ crédits = 0

Une force n'apparaît jamais seule, elle arrive toujours avec sa jumelle égale et opposée. Dans ce ledger, la valeur fonctionne exactement de la même façon. Elle n'atterrit jamais sur un compte sans une écriture égale et opposée sur un autre, et les deux maintiennent les livres à l'équilibre.

Les 100 USDC que vous débitez sur votre custody et les 100 USDC que vous créditez à l'utilisateur ont le même montant mais ne sont pas la même chose, l'un est un actif que vous détenez désormais, l'autre un passif que vous devez désormais. Donc rien ne bouge ici, les deux côtés grandissent en même temps, un nouvel actif et un nouveau passif égal apparaissent ensemble, et c'est exactement pour ça que les livres restent équilibrés.

La règle stricte

Dans chaque transaction, le total des débits doit être égal au total des crédits. Une transaction qui ne s'équilibre pas n'a le droit d'exister nulle part.

C'est cette règle qui fait passer « ne jamais perdre d'argent » du statut de simple espoir à celui de propriété clé du système sur lequel vous travaillez. La valeur ne peut pas quitter un compte sans arriver dans un autre, parce que sinon la transaction ne s'équilibre pas. Votre custody ne devrait pas s'écarter de ce que vous devez à vos utilisateurs, parce que chaque mouvement enregistre les deux côtés à la fois. Quand quelque chose ne va pas, le ledger ne vous rend pas un solde discrètement faux, il rejette carrément l'écriture.

En pratique, le cœur d'un ledger, c'est cette seule vérification. Ici, les montants sont stockés sous forme d'entiers non négatifs en unités mineures (centimes, ou wei), et la direction est portée par un champ side plutôt que par un signe :

type Entry = { accountId: string; side: "DR" | "CR"; amountMinor: bigint; assetId: string };
 
function assertBalanced(entries: Entry[]) {
  if (entries.length === 0) throw new Error("empty txn");
 
  const net = new Map<string, bigint>();
 
  for (const e of entries) {
    if (e.amountMinor <= 0n) throw new Error(`bad amount: ${e.accountId} ${e.amountMinor}`);
    const delta = e.side === "DR" ? e.amountMinor : -e.amountMinor;
    net.set(e.assetId, (net.get(e.assetId) ?? 0n) + delta);
  }
 
  for (const [asset, balance] of net) {
    if (balance !== 0n) throw new Error(`unbalanced: ${asset} off by ${balance}`);
  }
}

Deux choses là-dedans comptent plus qu'il n'y paraît. L'équilibre est vérifié par actif, donc une transaction qui touche des dollars et du bitcoin doit s'équilibrer dans chacun indépendamment, une devise ne peut jamais en régler discrètement une autre. Et les montants sont des bigint, pas des nombres à virgule flottante, parce que la virgule flottante et l'argent, c'est une autre histoire d'horreur pour un autre jour.

Encore une chose à remarquer : nulle part dans ce snippet on ne touche à une colonne balance. Le code vérifie seulement que les écritures s'équilibrent, il n'écrit jamais de solde nulle part. Et c'est le bon point de départ, un solde n'est jamais un fait que vous fixez directement. Vous n'écrivez pas balance = 500. Vous additionnez les écritures, debits - credits, lues selon le côté normal du compte. Les écritures sont la vérité. Le solde n'est qu'une question que vous leur posez.

Voici maintenant une autre chose sur laquelle on peut se tromper. Être défini par ses écritures ne veut pas dire que vous les additionnez à nouveau à chaque lecture. Si vous faites ça, vous sommez tout l'historique d'un compte à chaque fois, c'est du O(n), et n ne fait que grandir. Le compte que touche chaque transaction, votre trésorerie par exemple, finirait par être la lecture la plus lente. Donc vous ne refaites pas l'addition. Vous stockez le solde, vous le mettez à jour dès qu'une écriture arrive, et vous le gardez synchronisé avec le journal. Le journal reste la source de vérité. Le nombre stocké n'est en réalité qu'un cache qui doit être d'accord avec lui. Dérivé dans le sens, stocké en pratique, les deux, pas l'un ou l'autre.

La même idée en une image, une transaction qui se déploie en deux écritures équilibrées qui atterrissent sur deux comptes :

deux écritures, deux comptes

Pourquoi c'est la fondation

Un système en partie simple vous demande de faire confiance à un nombre. Un système en partie double refuse de laisser un nombre tenir tout seul, un solde n'est pas quelque chose que vous affirmez, c'est quelque chose que les écritures obligent à être vrai. C'est aussi de la même façon qu'un index unique rend les doublons impossibles plutôt que simplement déconseillés, la règle d'équilibre fait de la perte d'argent une écriture qui ne sera jamais commitée plutôt qu'un écart que vous découvrez des mois ou des années plus tard, avec votre licence remise en question devant une commission. C'est pour cette raison que presque tous les systèmes financiers sont construits de cette façon.

Cette première partie n'est d'ailleurs que le squelette. Un vrai ledger doit gérer des fonds qui ne sont pas encore réglés, un dépôt crypto en attente de confirmations de bloc, un retrait qui pourrait échouer et devoir être restitué à l'utilisateur qui l'a initié. Dans la partie II, nous allons appliquer ce modèle à de vrais mouvements, et voir un seul solde en devenir trois.

•••
Partager cet article :
Série
Série Ledger

Restez informé

Recevez les derniers articles dans votre boîte mail.