Now-Next

SaaS-architectuur

Bedragen in een SaaS-product: centen, geen floats

De tweede onomkeerbare beslissing in een SaaS-product gaat over geld. Waarom een komma-getal je centen kwijtraakt, en waar het ook met centen misgaat.

Chris van Eijk · · 8 min lezen

Dit is de tweede van drie beslissingen die je in een SaaS-product later bijna niet meer rechttrekt. De eerste is hoe je klanten uit elkaar houdt. Deze gaat over geld, en hij is verraderlijker, want hij gaat maandenlang goed.

Een verkeerd getaltype geeft namelijk geen foutmelding. Het geeft een uitkomst die er goed uitziet, waarvan er één op de zoveel duizend een cent naast zit. Je merkt dat niet in een test die je zelf verzint. Je merkt het doordat een klant zijn eigen optelsom maakt en er anders uitkomt dan jij.

Waarom een komma-getal geen geld kan bewaren

Een float of double slaat getallen op in machten van twee. Tienden en honderdsten zijn daarin niet exact uit te drukken, net zoals een derde in ons tientallige stelsel niet exact op te schrijven is. Wat er dan gebeurt is niet theoretisch:

  • 0.1 + 0.2 levert 0.30000000000000004 op, in vrijwel elke taal die IEEE 754 gebruikt.
  • 1.005 is in werkelijkheid net iets kleiner dan 1.005, waardoor de gebruikelijke afrondtruc — vermenigvuldigen met honderd, afronden, delen door honderd — hem naar beneden afrondt in plaats van naar boven.

Op één regel is dat een fractie van een cent en zie je niets. Het probleem is dat een boekhoudkundig product niet één regel heeft. Het heeft een factuur met regels, een btw-berekening per tarief, een totaal, een dagboek, een grootboek en een jaaroverzicht — en elke laag telt de vorige op. De afwijking reist mee omhoog en komt uit op de plek waar hij het meest opvalt: de balans die niet sluit.

Dat is de eigenlijke schade. Niet de cent, maar het vertrouwen. Een boekhoudproduct waarvan de klant de optelling moet controleren, heeft geen reden meer om te bestaan.

Wat er wel hoort te staan

Bewaar bedragen als hele eenheden in de kleinste munteenheid, in een integer. Voor euro’s zijn dat centen. Geen komma, geen decimaal type dat per database anders afrondt, en zeker geen tekst.

Twee dingen horen daarbij:

De munt reist mee met het bedrag. Een getal zonder munt is geen bedrag maar een aantal. Zodra er ooit één klant over de grens komt, is de vraag welke munt het was niet meer te reconstrueren — en die vraag komt altijd later dan het besluit.

Er is één type, en de rest van de code kent alleen dat type. Zodra een bedrag in twee vormen door het systeem kan lopen, is er een plek waar iemand ze bij elkaar optelt zonder dat op te merken. Eén type met optellen, aftrekken en vergelijken erin ingebakken is de goedkoopste vangrail die er is, precies omdat hij het verkeerde onmogelijk maakt in plaats van onwaarschijnlijk.

Waar het ook met centen nog misgaat

Integers lossen het optellen op. Ze lossen het delen niet op, en delen is waar het echte werk zit.

Zodra je een bedrag verdeelt — btw over een regeltotaal, een korting over een factuur, een abonnement over een halve maand, een rekening over vier leden van een vliegclub — komt er een rest die niet op te delen valt. Drie mensen die 10 cent moeten delen krijgen ieder 3 cent, en er blijft er één over. Wie krijgt hem?

Dat is geen strikvraag maar een productbeslissing, en hij moet ergens opgeschreven staan. De aanpak die zich in de praktijk houdt is: verdeel naar beneden, tel wat er overblijft, en geef de resterende centen één voor één uit aan de regels met de grootste rest. De som van de delen is dan gegarandeerd het geheel. Doe je dat niet expliciet, dan doet elke berekening het net iets anders en heb je een verschil van een cent dat niemand kan verklaren.

Twee dingen die daar direct achteraan komen:

  • Rond één keer af, op het laatst. Twee keer afronden in een keten is hoe een cent verdwijnt. De tussenstappen mogen niet netjes zijn.
  • Rond alleen af waar het antwoord naar buiten gaat — op de factuur, in de aangifte, op het scherm. Niet in het midden van een berekening omdat het er dan leesbaarder uitziet.

De test die het bewaakt

De test die hier telt, controleert de uitkomst en niet de code. Dat klinkt vanzelfsprekend en het is het niet: de gewone reflex is om te toetsen dat de functie de afrondmethode aanroept. Die test blijft groen terwijl het antwoord verkeerd is.

Wat je wél wilt weten, is dit:

  • Telt de som van de regels exact op tot het totaal, voor een reeks bedragen die niet mooi deelbaar zijn?
  • Blijft dat kloppen als er een korting overheen gaat, en daarna nog een btw-berekening?
  • Krijg je hetzelfde antwoord als je dezelfde factuur twee keer laat berekenen — ook als de regels in een andere volgorde staan?

Die laatste is de nuttigste en de minst geschreven. Een verdeling die van de volgorde afhangt, is een verdeling die na een migratie een ander antwoord geeft dan ervoor.

Bij Pilot-Next valt precies dit samen in de afrekening van een vlucht: vlieguren, brandstof en een clubbijdrage komen bij elkaar in één bedrag dat op het saldo van een lid terechtkomt. Er is geen enkele plek waar een halve cent mag blijven hangen, want dat saldo blijft staan tot iemand het opneemt.

Wat het kost om het later te doen

Vooraf is dit een dag werk: een type kiezen, de kolommen op een integer zetten, één verdeelfunctie schrijven en er tests omheen.

Achteraf is het iets anders. Je hebt dan productiedata waarin de fout al zit, en die kun je niet zomaar omzetten — want welk van de twee getallen was het juiste, het opgeslagen totaal of de som van de regels? Bij elke factuur die al de deur uit is, is het antwoord: geen van beide is nog te veranderen. Je migreert dan niet alleen je datamodel maar ook je geschiedenis, en dat is precies het soort project waar geen enkele klant voor wil betalen.

De derde

De laatste van de drie gaat over hoe je rechten modelleert — rollen in plaats van vinkjes per gebruiker. Die is minder wiskundig en net zo hardnekkig.

Wil je dit soort beslissingen niet zelf hoeven maken: dat is precies het werk waarvoor wij een SaaS-product bouwen.

Laten we praten

Wat gaan we bouwen?

Eén gesprek is genoeg om te weten of we bij elkaar passen. Vertel wat je voor je ziet — wij zeggen hoe snel dat kan, en of wij daar de juiste partij voor zijn.

Start het gesprek