Development
How a simple concept turns out to be complicated to implement
SoftwareSoftware Engineer Gijs Hendriks tells about a project that has stayed with him ever since.
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 currencies1, 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. 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 library3, 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.
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:
- Scalable
- Thoroughly tested
- Future-proof
- Centralised
- Consistent, precise, and predictable
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 Mills4 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:
- Extensive unit testing.
- Mutation testing5 is a really cool technique that gives you an indication of how effective your unit tests are.
- Mirror testing. When doing a calculation, do it the old way and the new way and check if there is a difference.
- Do a soft release based on IP. You would not give all customers the ability to pay with the new currency when going live, but only a subsection. Or even just people in the office at first.
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.
Β―\_(γ)_/Β―