Deux types de transaction, c’est tout ce qu’il faut

remboursements

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.

L’en-tête et le détail d’une transaction : €186.40 payés par vous, partagés en quatre, €139.80 qui vous reviennent

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.

« donné 15 à Anna Lopes », lu comme un virement de €15.00 de votre part à Anna Lopes

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