Une première version de HiBill savait enregistrer de l’argent qui change de mains de quatre façons différentes : une dépense, un prêt, un remboursement et un règlement. Chacun avait sa table, son écran, ses règles.
Aujourd’hui, il n’en reste que deux. Voici pourquoi, et ce que nous avons appris en les démêlant.
Trois objets qui n’en faisaient secrètement qu’un
Regardez ce que trois des quatre stockaient vraiment.
Un prêt, c’était : de l’argent est passé de A à B, et maintenant B doit de l’argent à A.
Un remboursement, c’était : de l’argent est passé de B à A, annulant une partie de ce que B devait.
Un règlement, c’était : de l’argent est passé de B à A, annulant une partie de ce que B devait — et nous le marquions comme définitif.
Trois noms. Un seul événement. De l’argent est passé d’exactement une personne à exactement une personne. Les différences relevaient toutes du récit — ce que l’utilisateur voulait dire — et non de la structure. Et nous avions inscrit ce récit dans le schéma, où il s’était figé en trois chemins de code à garder synchronisés pour toujours.
L’indice, c’était le champ de statut des règlements. Les règlements avaient les états pending, paid et cancelled. L’interface mettait paid à chaque fois, sans exception. Nous avions construit une machine à états, puis ne nous en étions jamais servis, parce que dans la vraie vie, on enregistre un règlement après que l’argent a déjà circulé. Un état mort dans un schéma est un signe fort que l’objet ne devrait pas exister.
Ce qui diffère vraiment
Une seule distinction a résisté à l’usage réel : le nombre de personnes à chaque bout.
Une dépense va d’une personne à plusieurs. Quelqu’un (ou plusieurs personnes) a payé, et un groupe de personnes se partage le coût. La partie intéressante, c’est la répartition.

Un virement va d’une personne à une autre. L’argent a quitté une personne et il est arrivé chez une autre, en entier. Il n’y a aucune répartition à calculer — le destinataire reçoit le montant complet.
C’est une vraie différence structurelle, et c’est la seule. Voilà donc les deux types.
Tout ce que l’ancien modèle à quatre objets savait exprimer, celui-ci l’exprime :
Ce que vous voulez enregistrer | Comment |
|---|---|
Un dîner, partagé entre cinq | Dépense |
Prêter €50 à un ami | Virement |
Rembourser ces €50 | Virement, dans l’autre sens |
Ramener un solde à zéro | Virement |
Régler n’est plus une cérémonie à part. Il n’y a ni écran dédié ni statut séparé. C’est un virement qui se trouve ramener un solde à zéro, et il apparaît dans l’historique à côté de tout le reste, parce que c’est exactement ce qu’il est. En enregistrer un, c’est comme enregistrer n’importe quoi d’autre : tapez donné 15 à anna, et le récapitulatif le lit comme un virement — vous avez donné €15.00 à Anna Lopes — sans aucune répartition à vérifier.

Les invariants réunis au même endroit
Fusionner les types a permis d’énoncer les règles une seule fois et de les appliquer une seule fois. Un seul fichier répond à la question « cette transaction est-elle valide ? », et il dit en gros :
Pour une dépense : quand il y a des payeurs, la somme de leurs montants doit être égale au total — une dépense peut aussi démarrer vide, avec des personnes ajoutées plus tard ou qui prennent leur part via un partage. Les participants en doivent chacun une part.
Pour un virement : exactement un payeur, exactement un destinataire, et la part du destinataire est le montant complet.
Voilà l’intégralité des règles propres à chaque type. Quand nous avions quatre objets, des règles équivalentes étaient éparpillées dans quatre services et avaient fini par diverger — le chemin à plusieurs payeurs faisait des vérifications que celui des règlements ne faisait pas. Aujourd’hui, il n’y a qu’une porte, et tout passe par elle.
La seule exception
Il existe dans la base de données un troisième type que vous ne créez jamais : Simplification.
Quand le système repère une boucle de dettes — vous devez de l’argent à Marco, Marco en doit à Aya, Aya vous en doit — il peut réduire cette boucle sans changer ce que chacun a réellement déboursé. Ces ajustements doivent être enregistrés comme de vraies entrées, sinon les soldes bougeraient sans explication.
Ce sont donc des transactions, marquées Simplification, posées dans l’historique, là où vous pouvez les voir. La règle que nous nous sommes imposée : aucun solde ne change sans raison visible. Un chiffre qui dérive en silence est pire qu’un chiffre un peu trop élevé.
Est-ce que ça valait le coup ?
La réécriture a supprimé plus de code qu’elle n’en a ajouté, ce qui est généralement bon signe. Mais le vrai gain, c’est ce qui est devenu plus facile ensuite.
Le partage ouvert, l’attribution des articles d’un reçu, les soldes en plusieurs devises, l’ajout de personnes non inscrites — chacune de ces fonctions aurait demandé quatre implémentations, une par objet, et chacune avait donc été discrètement repoussée. Avec deux types et des invariants communs, ce sont devenus de petits chantiers.
Voilà l’argument pour simplifier un modèle de données, et ce n’est pas vraiment une question d’élégance. Chaque objet supplémentaire dans un schéma multiplie le coût de chaque fonction que vous ajouterez un jour. Nous payions tout 4 fois plus cher pour préserver des distinctions qui n’existaient que dans nos têtes.
Les deux types sont en service dans HiBill dès aujourd’hui. Si vous voulez le détail du fonctionnement des soldes qui reposent dessus, comprendre les soldes en est le complément.
À lire aussi
Comment fonctionne HiBill : un carnet de comptes discret entre amis — tout le modèle d’une traite : les personnes, les soldes et les deux types de transaction.
Simplification des dettes : comment les dettes croisées s’annulent — d’où vient le troisième type, celui que crée le système.
Pourquoi nous refusons de convertir vos devises — un autre choix de modèle de données qui garde les soldes honnêtes.