How a simple concept turns out to be complicated to implement
The crux of multi-currency when expanding internationally at Gomibo
Introduction
Since the start of January 2021 we have gone live with 29 European domains. We want to provide all our customers with the best experience possible, so that includes paying for your order in your own currency. Despite the Euro being widespread, it is definitely not the only currency in Europe. Actually, there are 29 different currencies (1), which is on average more than 1 currency per 2 countries! Keeping track of different currencies in your code is not hard per se, but all of the implications, however, are a different story.
In addition to internationalisation, Gomibo will offer our software to other telecom providers as Software as a Service (SaaS) (2). As potential customers might not even be located in the EU, a prerequisite is that all of our systems are able to handle amounts in any currency.
I am Gijs Hendriks, a 28 year old Software Engineer at Belsimpel/Gomibo (3). I love cats, cooking, and working on complex challenges like this multi-currency project. This has been a large and important project, which is especially exciting when you have only been with the company for a couple of months. In this blog I will describe the challenges we have faced so far and our approach to solving them.
The problem
The first step in this project was to identify all the places in our system where we use prices or amounts in the code. Turns out, when you are a web shop, almost all your systems interact with money. From our communication templates to our price decision system.
What we discovered, is that all our amounts are stored in floats. Floating point imprecision can be circumvented by using the BCMath library (4), but this is an ugly, round about solution to the underlying problem. While this code has served us well for quite some time, we really want to modernise the system.
$x = 8 - 6.4; // which equals 1.6
$y = 1.6;
var_dump($x === $y); // false
As Churchill said: “Never let a good crisis go to waste”. This crisis is a good opportunity to throw away the old code and build a new solution that is:
Recommended by LinkedIn
Our solution
We looked at industry standards and best practices and as we expected, the solution is pretty simple. Use a 𝙼𝚘𝚗𝚎𝚢𝙰𝚖𝚘𝚞𝚗𝚝 that has a 𝚖𝚒𝚗𝚘𝚛_𝚞𝚗𝚒𝚝_𝚊𝚖𝚘𝚞𝚗𝚝 and a reference to a 𝚌𝚞𝚛𝚛𝚎𝚗𝚌𝚢 . The 𝚖𝚒𝚗𝚘𝚛_𝚞𝚗𝚒𝚝_𝚊𝚖𝚘𝚞𝚗𝚝 stores the smallest used amount in that 𝚌𝚞𝚛𝚛𝚎𝚗𝚌𝚢 . For euros that would be euro cents, but for the Japanese Yen, that would be whole Yens. This way you can do the calculation with integers instead of floats. We implement the 𝙼𝚘𝚗𝚎𝚢𝙰𝚖𝚘𝚞𝚗𝚝 class and provide services to perform operations on 𝙼𝚘𝚗𝚎𝚢𝙰𝚖𝚘𝚞𝚗𝚝𝚜 like comparison, addition, subtraction, allocation and formatting.
We need more however: we need to be able to send accurate invoices to companies and private individuals in different countries. So, we have created a 𝙿𝚛𝚒𝚌𝚎𝙰𝚖𝚘𝚞𝚗𝚝 that contains a 𝙼𝚘𝚗𝚎𝚢𝙰𝚖𝚘𝚞𝚗𝚝 and a 𝚅𝙰𝚃𝚝𝚢𝚙𝚎 . We assume that the 𝙿𝚛𝚒𝚌𝚎𝙰𝚖𝚘𝚞𝚗𝚝 always includes VAT and use a service to convert to a different VAT rate. Internally, this uses the allocation we implemented for a 𝙼𝚘𝚗𝚎𝚢𝙰𝚖𝚘𝚞𝚗𝚝.
Sometimes we want to convert a price to a different currency. We need a timestamped conversion rate for that. So, we create a third object: a 𝙲𝚘𝚗𝚟𝚎𝚛𝚝𝚒𝚋𝚕𝚎𝙿𝚛𝚒𝚌𝚎𝙰𝚖𝚘𝚞𝚗𝚝 . In this object we provide a link to a 𝚌𝚘𝚗𝚟𝚎𝚛𝚜𝚒𝚘𝚗_𝚛𝚊𝚝𝚎_𝚌𝚑𝚊𝚗𝚐𝚎 , which contains the conversion rate that applies to that amount. Now we can show our euro-centered customer service the amount the customer paid in Danish Crones and the estimated amount in another currency they are familiar with (Euro). Also, a customer will always see the same price they did when they ordered, even if they return to their order some days later when the exchange rate changed.
Upcoming challenges
So everything is great, I am happy with this solution. But there are still some challenges that remain unresolved. For example: the transaction costs that are charged to us by one of our payment service providers are not in whole euro cents. And with this system, we cannot handle sub-cent precision, since the whole point of this system was to not use decimals… Sigh. This difference will add up over time in balance sheets (have you ever seen the movie Office Space?). As imbalance in balance sheets makes accountants uneasy, we cannot ignore this unfortunately. A possible solution is to create an additional class that can handle more precision, something like a 𝙿𝚛𝚎𝚌𝚒𝚜𝚒𝚘𝚗𝙿𝚛𝚒𝚌𝚎𝙰𝚖𝚘𝚞𝚗𝚝, where you specify the amount of Mills (5) and maybe even more decimals as ints. We can also make separate services that you can use when working with sub-cent precision.
Releasing a large feature like multi-currency support is a little precarious. Because you are changing a lot of code at once, the chance you introduced a bug somewhere is bigger. And on top of that, the changes you are making have a big impact if they break. You could suddenly owe the company a lot of money ;). You can prevent these issues with a couple of things:
This solution is mainly an exercise in system design. And one of our goals was to make our approach unified. We would like to get it right the first time, implement it and, boom you’re done. This means that the system should be able to handle any possible exceptions that we encounter. We try to account for this by including all requirements and exceptions in the rules that define our system but it is incredibly hard to account for everything. This is a big reason why the waterfall system fails so often and buzzwords like agile and lean are penetrating all kinds of sectors. So, we use an iterative approach (as we always do). As a consequence of this, we have already re-written our core classes a couple of times and I expect to do it a few more. In fact, it’s likely this blog is already outdated by the time you read it.
But I guess that is just the life of a software engineer. And that is part of the fun.
¯\_(ツ)_/¯