Development

Hoe een simpel concept ingewikkeld blijkt te zijn om te implementeren (Engelstalig artikel)

22 maart 2021

Software Engineer Gijs Hendriks tells about a project that has stayed with him ever since.
Note: dit artikel is alleen in het Engels beschikbaar.

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.

Β―\_(ツ)_/Β―