In my last financial product we added 'bitcoin' and 'festival tokens' as currencies in the backend. At first for fun.
But we found out the client devs (web, mobile apps) had severe difficulties and wanted it removed. Turned out they all implemented currencies wrong. Either with hardcoded decimals or with localised in and outputs that would break if a client changed their locales.
So now, my favorite best practice for any financial data handling is that: ensure your system can handle Bitcoin (8 decimal places) and festival tokens (missing currency symbol, zero decimals). Anywhere this leads to trouble is a red flag and will probably cause trouble later on. Now at least you are aware.
German has a (few?) instances where going from lowercase -> uppercase -> lowercase or view versa is ambiguous because there are multiple upper/lower variants for each letter. I also think one even changes the text length because it turns into two.
Round trip = sending it from the input/front end into the system/back end (whatever it is, database/file system/???) and back to the front, to confirm that what comes out is what you put in.
For German, I'd assume it's just about the special characters (äöüß), ie testing that encodings are somewhat correct (at least beyond ASCII).
Yeah using German translations is a well known simple strategy to see how your site handles longer word length, because of the language characteristics.
It will do a good job of pointing out places where you haven't built in adequate flexibility for longer words. It's not perfect of course, there are times when a translated word is shorter, but it is a nice real world test of the basic flexibility of your page layout.
- An even 30 days = Easy to explain and do math with. Adding/subtracting 1 month can leave you in the same month or skip February entirely! Anniversary dates bounce around from month-to-month. Maps poorly to yearly calculations.
- However many days >this< month has = Harder to explain and do math with. Months without 31 days can get skipped when adding/subtracting 1 month. Recurring events get pushed away from the end of the month to cluster around the beginning of the month. It does result in a steady anniversary date for edge cases.
- Closest numeric day, but prev/next month (e.g. Jan 31 -> Feb 28): Easy and hard to explain and do math with. It's great for ensuring events only ever happen once per calendar month, but adds extra ambiguity (e.g. Jan 31 > Feb 28 > March 30? ... or March 28th?)
- 30.4... = Just no... except for the few times this is right and you need consistency to avoid unfairly comparing 28 days vs 31 days.
The consequence is some very surprising things the first time you see them. Some billing systems simply avoid doing work past the 28th of each month (either doing it a bit early or a bit late). Some just embrace the weirdness (whichever you flavor you pick) and you get used to the quirks (e.g. end-of-month lulls and start-of-month spikes).
If I ever get to redesign the billing system at work, I'm going to go with fixed day of the month billing. We currently do 30 day billing, and most customers are fine with that, but there are some who need their bill to not come the same week as their rent is due, or not come until after they receive their monthly pension payment, or something like that. Right now we have to have special handling for those people.
For handling the different lengths between the months, I've considered a few approaches.
1. If re-bill is supposed to be the Nth of each month, that simply means N-1 days after the 1st. If that falls into another calendar month, fine. So if someone asks to be billed on the 30th of each month, they will be billed on Jan 30th for January, Mar 2nd for February, Mar 30 for March, and so on. I'd make sure the receipt explicitly says "Re-bill for January", "Re-bill for February", etc., so that bill the see on Mar 2nd hopefully will not confuse them, nor will seeing another bill later that month.
2. If re-bill is supposed to be on the 29th-31st, for February it takes place on the 28th (29th in leap years). For other months, if re-bill is supposed to be on 31st, it takes place on the 30th in April, June, September, and November.
3. If you initially order on day 1-21 of the month, your re-billing is the same day ever month. If you initially order on day 22+, your re-billing is on the same number of days before the end of the month as was your initial order. For example, someone who orders on May 30th would re-bill on the 30th in 31 day months, the 29th in 30 days months, and the 27th in February (28th in leap years). If you ask for a change in fixed billing date, you can do so either as Nth day of the month or Mth day from the end of the month, but N or M must be <= 28.
If you ever get to... GET TO? You must love pain! Actually, I wonder what business you're coding for to have those be important criteria and wish you all the best of luck!
In general, however:
Don't fall victim to one of the classic dev blunders -- the most famous of which is "don't roll your own crypto" -- but only slightly less well-known is this: "Never write your own billing logic when business is on the line!"
> For handling the different lengths between the months, I've considered a few approaches.
> 1. If re-bill is supposed to be the Nth of each month, that simply means N-1 days after the 1st.
I have a credit card that takes the much simpler approach "Your payment due date is the Nth of each month. N can be any value from 1 to 24."
This seems to basically match your approach 3.
If you ask how I think you should bill ( :p ), do it on a fixed day and don't try to do rebilling at a fixed interval. If the day somebody buys your thing is unsuitable for fixed-date billing, the answer is prorating their first period, not interval billing.
> If I ever get to redesign the billing system at work, I'm going to go with fixed day of the month billing.
This may have unintended consequences. Some examples we faced around this:
1. Stress on our APIs when 15,000 invoices are created in the span of a couple of minutes. We had to build more queueing mechanisms around this. Some services just can't handle more than 100 calls a second.
2. Getting rate limited by MasterCard
3. Some payment providers thinking we're brute-forcing them
Aren't all of these handled by a queue and some reasonable sense of going through it slowly? (I can just say that on the internet, I know designing it that way is not exactly trivial).
For additional fun, consider how this would work with the added constraint of needing the date be a business day, so you need to take into account how weekends (which vary across the world, by the way; in a bunch of countries sunday is a business day but friday is off) and banking holidays in every locale in which you do business.
For reference, in China there are a bunch of legally mandated holidays that convert the weekend days around them into normal working days. (The conversion is not legally mandated; think of it as a response to laws that generate many more holidays than the economy supports.)
Depending on the power-relationship with any given customer, you may not get the option to dictate this - sometimes the customer tells you want it will do and you have to follow. Other times you can dictate but without the flexibility, you put the company at a competitive disadvantage.
Don't forget that calendar math is not commutative!
Jan 27 + 5 days = Feb 1
Feb 1 + 1 month = Mar 1
Jan 27 + 1 month = Feb 27
Feb 27 + 5 days = Mar 4 (or Mar 3 in a leap year!)
Therefore...
Jan 27 + 5 days + 1 month = Mar 1
Jan 27 + 1 month + 5 days = Mar 1
AND
Jan 27 + 5 days + 1 month = Mar 4 (or 3)
Jan 27 + 1 month + 5 days = Mar 4 (or 3)
Before someone claims that this is just a February problem, please first consider that you can create these scenarios with any set of months that have differing lengths.
Then, please consider that your argument that ignoring February is not a good way to approach any calendaring library....
All too familiar. Luckily, I have built "date time" functions for a relational DBMS before!
I had specifically worked on functions like EXTRACT to pull out different parts like ISO Week numbers, as well as DATEADD and DATEDIFF functions which do date math. It is exceptionally unpleasant!
I'll unwaveringly go with whatever the hardass valid-date-enforcing mechanisms in java.time and PostgreSQL enforce.
Add a month to the epoch time? Can't do it because that doesn't adequate indicate what day we think it is. "select '2021-01-30'::date + make_interval(months := 1)" says Feb 28? Done!
This still isn't a complete solution, or at the very least holds non-obvious implications. Specifically, date math is not commutative[0]. And, yes, Postgres or java.time might force any arbitrarily ordered expression to always evaluate the same, but this would be based on some special logic for canonicalizing arithmetic on date expressions.
Do they do this? I don't know.
Even if they could do this in a single expression, there are absolutely scenarios where two date-arithmetic operations may be performed in two separate places in code. If those operate on the same logical date value, then we will see the same commutativity issues.
Absolutely. Go with the date/time/interval functions that your database or language libraries provide. Do not try to reinvent this, you will get it wrong 10 times before you get it right.
One solution is to distinguish between a "datetime" and a "duration". You cannot convert a duration to days (or really anything). You can add or subtract a duration from a datetime, and you can convert the difference between two concrete datetimes into days.
And that's actually how some libraries already implement it.
So the answer to "how long is a month" is "it depends, and I can only say if you apply this duration to a concrete time".
You use the standard datetime libraries so you don't have to deal with all that complexity. Only, they automagically muck with the data, including irrelevant precision, such as time and timezone and daylight savings time for a calendar date.
And for the sake of completeness, your application allows the customer to specify a default timezone (usually that of their headquarters or something) and individual users to specify their own (useful if they work remote or in another office or something).
So now you have your database's timezone (and datetime handling), your server's timezone, your application's timezone (and datetime library handling), the customer's configured timezone, the user's configured timezone, their client's own timezone handling (which may be different if they're on a business trip in a different location), all screwing with the date of something that shouldn't even have a time, timezone, or daylight savings in the first place.
That's an endless whack-a-mole of bugs. And trying to just use UTC everywhere possible only gets you so far.
I can only imagine needing to toss in standard/daylight timezone switch in there. Billing code really is the pinnacle of developer pain. It mixes the arbitrariness of special case business and customer rules with the absolute horror of time and date math.
yes. I wrote a recurring billing system for a hosting provider, back in the early 2000's. I think I settled on min(day_of_original_order, num_of_days_in_month) for when an order would bill.
Oh boy, those were the days. I imagine you're well aware of the DreamHost debacle around their own home-grown billing engine!
Looking back I'm amazed at how easily companies got off the ground by hiring eager (but exceptionally green) young web devs to write sensitive code for ordering/sign-up, recurring billing, and more.
I ran into issues coding an interest calculator for personal usage when I realized that the length of a year is variable. My unit tests were failing because all the finance examples never explained how they dealt with time changing out from under the formulas they were using, and all the computer knew was Unix time.
I still don't exactly know how to handle this. If you start an account that compounds interest continuously, does the daily interest rate change if one year is longer than another?
What about just mapping it? That is, simply choose the smallest increment necessary for your system and _enumerate_ every value within a window as your source of truth. There is less than a million hours in 100 years. Is this system going to be around in a hundred years? Probably not.
It might make some of the math/algorithms a bit painful but I think you could guarantee correctness with the above approach.
I like "730.5 hours" (usually rounded to just 730). It's a good fit for services billed by the hour or minute. I use it regularly to answer "how much per month" for AWS stuff. But yeah - it fits in there with your "Just no..." category.
Maybe if the date is "close" to the end of the month (as in fewer than X days from the end of the month), then count in the days left, so Jan 31st (0 days left) becomes Feb 28th and then to Mar 31st and so on.
One place I worked did recurring payments on the day of the month you first set up recurring billing. If the date is the 31st, it got moved to the 30th. Feb 28 was...problematic for billing load.
I would base anything dealing with money on a double entry accounting system.
The issues that arise are then a question about how to translate them to journal entries, which will require accounting experience, and maybe your chart of accounts needs to be revised, but the core system should be fairly stable, and voiding an invoice by issuing a credit note comes automatically, as you’re working with an append-only ledger.
The issues about prepaid plans should also be handled, as payments for services not rendered yet should not be recognized as income, but instead kept as a liability: The OP mentions their system is used by 15,000 customers, so I would give each customer their own account (in the chart of accounts).
The main technical issue is what datatype to use for money, the rest are problems solved by following general accounting principles, though I fear that a lot of programmers out there are reinventing the wheel (in suboptimal ways).
> payments for services not rendered yet should not be recognized as income, but instead kept as a liability
This entirely depends on whether you are operating on a cash or accrual basis. Both approach are valid (at least in the USA) and cash basis is often used by small businesses.
I use the word “should” as per RFC 2119, i.e. recommended (as opposed to “must” = required).
Though the IRS does limit the types of businesses that can do cash-basis accounting, it would be businesses without inventory, and who does not offer credit to their customers, e.g. a hairdresser would probably use cash-basis accounting, but more complicated businesses would not, certainly not a business that needs its own billing system :)
I fumbled and bumbled my way to this realization while trying to build a billing system intended to tolerate all sorts of invoicing and reversal scenarios.
More precisely, it doesn't matter what the nature of the billing is, ie recurring (monthly, quarterly..etc) vs one-time; You want to build a system around invoicing for discrete items and resolving those invoices against various criteria (payment received, credits issued, cancelled plans...etc).
You may want to look into 'event sourcing' too. Which is an architectural pattern, very well suited to deal with most accounting problems you describe.
I've built a billing system for a hosting platform. Back when all I knew was MVC (and the then common index.php ballofmud). I wish I had known about eventsourcing then, because so many problems that I spent weeks on, would have never occurred, or been solved in hours.
Eventsourcing comes with its own downsides, requires your mindset to change (esp hard in a team that has been doing relational databases or MVC for years), and is convoluted. But it is a very good fit for a large swath of problems. Financial ones the most.
> 'event sourcing' […] is an architectural pattern, very well suited to deal with most accounting problems
I believe double entry accounting can be described as following this architectural pattern (despite predating it with hundreds of years).
Double entry accounting has both a ledger and a journal. The ledger describes the actual transactions, the journal groups multiple transactions and assigns a “why”, it can also link the journal entry to an invoice, receipt, credit note, or user who caused the journal entry to be created.
So the journal is your event log (not the ledger).
> Eventsourcing comes with its own downsides, requires your mindset to change
Similar to double entry accounting: The learning curve I would say is the “chart of accounts”, how to express everything as ledger transactions, be it tax, fees, discounts, credits, prepayments, etc.
But it can all be done, and once you understand the system, it becomes trivial and extremely flexible.
A rule of thumb is that any number in the interface should come from the ledger, e.g. if you issue an invoice and give your customer a discount, there must be a ledger entry corresponding to that discount.
> I fear that a lot of programmers out there are reinventing the wheel (in suboptimal ways).
How hard can it be???
<goes away and starts writing a new javascript framework from scratch that addresses precisely _one_ of the billing scenarios, and depends on 17,000+ npm libraries, including leftpad.js>
Having worked on billing and accounting systems for decades now. I am constantly amazed by all of the edge cases and situations that have to be handled to ensure that every bill is correct and nothing is missed or misbilled. Especially since the oil firms out here will reject an invoice if it is off by as little as a penny.
At first it seems like it's so easy and straightforward but when you are looking at thousands if not millions of bills a year and the myriad methods you need to compensate for to ensure its accepted by large firms.... well. I can say nothing surprises me anymore. Including hardware level errors causing an issue no one expected with a final number.
Right -- accounting seems simple. It's just debits and credits and adding it all up. Until you get into it, and you understand why "Accounting" can easily be a department of dozens or hundreds of people.
> Especially since the oil firms out here will reject an invoice if it is off by as little as a penny.
Half of them will reject you for being wrong, the other half will reject you for being right because it doesn't line up with their floating point errors in excel.
Another fun use case is when a customer is charged for their invoice via credit card and then 60 days later, you get a chargeback from the bank. Now you have to unwind the previous invoices and entitlement systems to figure out if you should fight the chargeback, cancel the subscription or some other non-standard process.
Here's a lesson (I can laugh about now that I'm retired): one of the first billing systems I ever wrote (in the 90's) had invoices with a mask of '$ZZ,ZZ9.99' based on interviews with staff. About a year later I get an urgent call from the owner. It seems a client was under-billed about $100K dollars due to truncated invoice.
Subsequently, whenever interviewing clients for requirements I'd mention this and it usually resulted in padding their specs.
> Store money including the smallest possible subdivisions.
This is fine as long as everybody follows ISO4217. In my experience many 3rd party systems _mostly_ do it and of course you get bitten by the edge cases.
For example, Stripe decided HUF doesn't have 2 decimals (I know that's how it's used in practice today but we're talking standards and system interoperability here), or that ISK subdivides into hundreds ("cents") in contrast to the ISO tables. Compare https://stripe.com/docs/currencies with http://currency-iso.org/en/home/tables/table-a1.html. As Stripe notes for UGX, backing out of this is nearly impossible because of the subtle break in backwards compatibility. And who knows how other payment providers deviate in their own way.
For my own sanity and that of my coworkers (e.g. easier analysis for extracted tables in the data warehouse) I strongly prefer avoiding this headache and use decimal instead. You can do the appropriate rounding for a currency in your money object as for example https://www.martinfowler.com/eaaCatalog/money.html does.
One edge-case I've noticed with Apple's subscription billing and heard other people talk about when implementing billing systems is changing currencies/countries. Apple, in line with most people's expectations, when you cancel a subscription it doesn't cancel immediately and prorate a refund. It just stops future billing and lets you serve out your remaining cycle (gyms and other meatspace places do this). The problem is you then have to wait until your subscription expires to transfer your account to a new country/currency.
Yeah, this immediately broke my idea of only dealing with cents and integers. All of a sudden I wanted to price something $0.001 and it all came crashing down.
As the parent says it's the unit price (with quantity of millions) so the end amount will be rounded but all the steps before that need to be able to handle fractions.
"Whatever limitations you plan for, plan for how to bypass them too. This will happen."
The world is not straightforward, allow an admin to fix/change anything and when they get tired of making some change then code that new path in the system. Working with or writing systems that do not allow admin overriding is painful.
OK, I read the first bulletpoint and just thought "How did this get upvoted this much, this makes no sense":
> Money isn’t always decimal
> A common wisdom in database design is “never using floating-point numbers for money”. Some recommend using the MONEY datatype, while others tell you to use DECIMAL.
> Both of these are wrong. Sure, yeah, in the US and most of Europe, money is decimal.
> That’s certainly not the case in Japan – you can’t charge 2500.50 JP¥.
The author has a very odd definition of "Decimal". Integers are most certainly decimals. The fact that in Japan you can't charge fractions of a a yen doesn't mean that Decimal isn't actually an excellent choice to represent that currency, because it certainly can represent any yen value you would need. The whole reason you don't want to use float is because it cannot represent certain valid monetary values.
As others have pointed out, storing values as "the smallest division", e.g. pennies for USD, is just wrong, because there are many contexts where you have to represent fractions of a cent <insert Superman 3 joke here>.
There is a larger point here, that 2500.5 JPY is not a valid amount to charge, thus having it be possible is a problem.
Whereas 10000 USD cents is a valid value.
Integer storage of money is my favourite as well to be honest. It's easy to work with and reason about, and it just makes decimalization a display issue.
But as others have pointed out, actual value can be fractional, it's just not priced that way.
I mean, in the US it's actually pretty standard for accounting applications to use a scale of 4 (4 units to the right of the decimal point, the default for MONEY), even though people are never charged fractions of a cent.
Simplest example someone gave is gas station pricing, in which prices often end in 9/10ths of a cent - the price is calculated that way, and only rounded when the customer pays.
Isn't the "all prices are pennies" approach the way USD is handled in practice, like "$399.99"? Certainly no one does irrational divisions like "$24 1/3rd". There might be a small paradigm shift that USD or EUR users might feel if they are used to think of supplementary units genuinely as divisions of full units, but I think most systems do consider the main unit as a sum of smallest denomination and not the or any other ways around.
Those are unit prices or interval prices. Any transaction will almost definitely be in an amount of dollars and centers if dealing in US, rounding to a whole cent based on the total units or interval.
For example, if you're paying #3.089 a gallon at the pump, and you pump exactly one gallon, you don't pay $3.089, you'll pay $3.09, and neither your account or the account you're paying into will need to know or care about the tenth of a cent difference, because our monetary system doesn't really deal with denominations that small in transactions.
There a subtlety in what I was saying that's being lost (which means you aren't wrong, but what you're stating doesn't necessarily apply specifically to what I was trying to say). You don't round rates, or prices, but you do round accounts because those represent real money. Your account can't actually have a fraction of a cent in it. Either the account it came from keeps the cent, or your account gets it. In a very real way, an account can be thought of as an integer number of cents (because often it's implemented that way). That may or may not make sense for your inventory or service system, where sub-cent rates make sense because the number of units is often greater than one.
To bring this back to the original comment and the items it was referencing, store money (real money, like accounts) in the smallest possible subdivision (cents for USD), but that doesn't need to apply to pricing, which is a potential amount of money. Potential money needs to be changed into real money at the time of a transaction, and real money cares about quantities it's possible to have, and it's not really possible (or at least useful, in most cases) to have less than the smallest possible subdivisible amount of a currency.
You're entirely correct though that you can't assume that cents is enough to accurately model everything to do with a business that works in USD, and ignoring that will result in problems like you showed.
I worked on a gambling webapp a long time ago. Gambling is heavily regulated here (.au) and all financial calculations are required to be done with 1/10,000th of a cent resolution, and rounding is only allowed to be applied once at the final display step.
(I used the regulations as a way to ensure _all_ calculations happened on the (Java) backend that other people were responsible for, given Javascript's documented mathematical insanity meant any client side calculations couldn't possibly pass compliance...)
For instance, with proration. Take each line item, prorate it, round it, then add them all together. Totally reasonable.
Now add all the line items together, prorate the total, and round it. Also reasonable, but there's a decent chance the number is different by a few pennies because of rounding differences. If you chained more calculations the differences would compound.
Whichever way you choose, somebody is going to whip out their calculator and tell you that you did it wrong (but only when they come out better the other way).
This applies anywhere you're doing multiplication or division. Discounts, proration, taxes, "cashback", "store credit dividend", whatever.
In my world, we keep a running total of the rounded prorated amounts, and then the last item (n) becomes (total - total up to n-1) to make sure amounts match. It can still be a problem if the last value is very small however.
Yeah, that's another way to do it. Another way to look at that is you're doing the proration on the final amount, and then distributing the rounding difference across the line items. Which is nice because you get the right total, though it can look weird when line items for the exact same thing are off by a penny from each other.
I went through the same issues 6 years ago, had to create the entire billing system from scratch, and there are so many corner cases I had to deal with, specially with plan changes on recurring subscriptions, had to deal with world wide timezones for the invoice creations and lots of other stuff. I just wish there was a pluggable, robust and flexible solution to use instead of reinventing the wheel.
I'm glad to see he also has the same thought about floating points.
Also:
> Happily, Moonpig did not have to deal with multiple currencies. That would have added tremendous complexity to the financial calculations, and I am not confident that Rik and I could have gotten it right in the time available.
This is one of the things we deal with which is quite challenging.
There's also PO-based vs. CC-based payment which have different sequencing of the details. You have one I run into all the time: Net-30 vs. Net-60 vs. Net-90. As well as linkages to quotations and terms. B2B takes all this to another level.
To be fair, this blog is apparently written by a product manager, who presumably discovered these features by doing research. And probably asking finance teams.
Totally disagree. I came here for commentary on the blog post not a cliche HackerNews trope. When I replied, his was the top comment. Honestly, guys like that need a nudge every now and again to train their filter. A downvote just isn't enough. Welcome to the community.
The solution to bad comments is not making the thread even worse, and the solution to "cliche HackerNews trope" is not to add another cliche trope with more aggression. That only contributes to taking us further into hell. The intended spirit of the site is just the opposite of that. If you're familiar enough with HN to complain about tropes, I assume you're aware of this?
> A common wisdom in database design is “never using floating-point numbers for money”
Common, and in my experience totally wrong. It's the most pervasive cargo culting I've experienced amongst developers, where people with 0 experience with financial applications will recoil if you argue against it. In my time developing applications for front office at an investment bank, floating point often works best.
Of course in your case though, for a billing system, the method you describe is obviously the right one.
Floating point is bad as null are, and is defended by the same apologist of null: "I don't make mistakes... so is good!".
Floating point ARE a major source of errors across everyone that use them. ARE infectious. ARE semantically wrong. ARE not made for financial calculation.
ARE WRONG.
Period. Just because under a lot of discipline (or luck, or just "assume" is working but nobody have checked, or work before but how knows if today?) not make it a good choice for financial/money.
Is the same error when people think old String types can be used in the unicode world, instead of have a proper type for that.
Luck help a lot. But is not something to be proud about.
Finance isn't just banking and accounting. A huge amount of finance is modelling, simulations, signal generation, etc. where being fast is often way more important than being completely 100% accurate. In financial modelling, errors from floating point are going to be insignificant compared to all the other assumptions you make in your models.
I have worked in finance for my entire career, and everyone uses floating point arithmetic for dealing with money on the modelling side (sometimes $0.01 = 1.0, sometimes $1 = 1.0, depends on the institution/currency/convention/context, but we have to deal with fractional money anyway).
They do NOT use it on the accounting/back office side. That would be a spectacularly bad idea. Those systems are designed for accuracy to the penny.
Well no, the key difference between null and floating point is that for all its flaws, floating point is by far the fastest way of doing non-integer numerical computation. Null is just an ugly convenience hack of arguable merit, floats are fundamental.
They're not a major source of error when you're implementing a model which is already inexact to a far larger degree than the problems caused by floats. Black-Scholes does not perfectly price an option, your bootstrapped curve is not a perfect predictor of market conditions in 28 years time. These are the problems faced in front-office finance, the error is already so far beyond 1 + 0.1 not perfectly matching 1.1 that it's not worth caring about. I've worked on applications where users just wanted to see numbers to the nearest 100k so they could model out a few trades they planned to make over the phone.
When you're working on a retail banking app where you need to track customer's balances, or an accounting system, or anything like that, then sure, floating point would be malpractice. That is nothing like any of the applications I've worked on in the financial field.
You are getting a lot of down-votes, but you are correct, and I gave you some upvotes, at least one.
If the application is doing financial modeling and estimations, where only 1-3 decimal place of accuracy is needed, then floating point is the right choice. It greatly simplifies the app.
If the application is doing accounting, payments and billing, where people expect accuracy to the penny or more (e.g. to 1/10000 of a penny), then it needs to use a Decimal type.
Yes, every time someone categorically declaims that 'floats and money should never mix', I question whether they've ever met a real accountant.
Accountants frequently spend all day in Excel. Excel uses floats for all computations (doubles, to be precise) [1].
Now, in all fairness, an accountant and developer's relationship to the numbers is rather different.
For an accountant, the risk of floats blowing up is largely mitigated by the fact that they have a close, intimate relationship with the actual numbers.
The responsible accountant should always deliver numbers they have personally reviewed, while a developer is usually automating a process, generating numbers that have yet to touch a human eye, so there is rather less room for error.
Still, it's simply untrue that one should never use floats for money. Many of the cases where floats would generate bad results are also problematic for simple alternatives. For example, fixnums are simply 'floats that can't float', so you need to be able to guarantee a fixed range ahead of time.
Understanding basic numerical analysis is unavoidable to writing correct code.
How did you handle the properties of floating point? The reason people recoil at that more than likely has to do with lack of knowledge in how floats are wrangled to ensure they're accurate.
Which properties do you mean in particular? The main point is just that finance doesn't necessarily require perfect accuracy. It does when Bob sends Alice $1 and their account balances must line up perfectly, it doesn't when you're implementing models which already have greater error than floating point could imbue. Not to mention that decimals, integers and rationals cannot compute something as simple as compound interest without lack of accuracy, so they're not going to save you when you're implementing Black-Scholes.
I mean if you're storing a value of money higher than the accuracy of floating point - how are you handling it? In languages like python you can have numbers of whichever length you want - internally it's breaking up that value into multiple primitive integer or float types I assume.
Is that similar to what you do, or are you handling that differently?
Python has arbitrary precision integers but floats are limited to 64 bits (i.e. the maximum is 1.8e308). Still sufficient for most financial applications though.
(Continiously) compound(ed) interest is e^(rate * time), e is not a rational number. Transcendental functions are commonly used in financial modelling, and once they appear, nothing short of a full-blown CAS will give you 100% accurate results (not that you should care, because your model is off by more than floating point error).
In the context of a bulge bracket investment bank, it's basically like this:
Front office - S&T (sales & trading), i.e. revenue generating activity. Usually includes any quants/quant developers actively working on things that make money
Middle office - operations. handles settlements, confirms, and generally anything related to the post-trade flow that is "after the trade is booked"
Back office - accounting, legal, engineering, IT support, HR. Anything that isn't a revenue center that also isn't even tangentially facing revenue generating operations
FWIW, front office is the one place where floating points for money might be sensible, because in most cases in the front office you don't care about pennies. Instead, what you want to get right is derivatives ("risk"), which you compute with finite differences ("bumps") or somewhat more advanced methods (adjoint automatic differentiation, basically the backward propagation algorithm). So, yeah, if you deal with risk and PVs (in other words, maths) rather than actual cash flows, sure, knock yourself out, use floats.
That does not invalidate the general advice to avoid floats for money (which is probably why you were downvoted).
But we found out the client devs (web, mobile apps) had severe difficulties and wanted it removed. Turned out they all implemented currencies wrong. Either with hardcoded decimals or with localised in and outputs that would break if a client changed their locales.
So now, my favorite best practice for any financial data handling is that: ensure your system can handle Bitcoin (8 decimal places) and festival tokens (missing currency symbol, zero decimals). Anywhere this leads to trouble is a red flag and will probably cause trouble later on. Now at least you are aware.