Representing Money at Rocker
As a fintech company, we spend quite a significant amount of our time writing code that models money movement in various ways. What follows is a need for us to represent money in a safe and clear way. The first step is to represent monetary values in our system, the second step would be to operate on these values. Let’s explore a few different ways we could do this.
A naive approach
case class Money(amount: Double)
If you’ve never worked with money before and only operating in your own country, there might be a tendency to think extremely locally and omit the currency. It’s better if we make this explicit so we can support multiple currencies.
Improvement
case class Money(amount: Double/Float, currency: Currency)
This solution gets us quite far. There are a few issues with representing money using decimal values. Floating points are usually approximations of numeric values and can cause a lot of issues due to rounding errors. `Double` might lack the precision that we want. Decimal values in general tend to be quite difficult to deal with and we could instead represent money using integers.
case class MoneyMinor(amount: Long, currency: Currency)
When representing monetary values using integers, we set the amount for the currency in its smallest currency unit (also called minor units), e.g. 100 cents for 1 dollar or 1 for ₩1, South Korean Won being a zero-decimal currency. By choosing to do it like this, it makes it easy to represent it in all layers of our program, from API to DB. It’s also easy to operate on, e.g. we would model 3.15 USD + 5.73 USD as
315 + 573 = 888 // 3.15 USD + 5.73 USD
The downside of this approach is that there is no easy way to represent fractional amounts. How do you represent half a cent? There are some solutions to this, one is to use rational values to represent money. However, at Rocker we made the decision that this issue never has come up for us so we rather skip it since it bring a lot of obscurity.
Let’s make the scale explicit so that we do not need to worry about whether it’s minor or major units and that it’s part of the representation rather than just in the name.
case class Money(scaledAmount: Long, currency: Currency, scale: Scale)
This allows us to represent 100 USD either as
case class Money(10000, Currency.USD, 2)
// or
case class Money(100, Currency.USD, 0)
Operations
One of the things that we discussed internally was about how to add, subtract, divide, and multiply monetary values of two different currencies and how we can avoid mistakes where we add amounts of two different currencies. Is there a way for us to leverage Scala’s type system somehow for us to be able to make the type explicit?
Type parameters
case class Money[A <: Currency](
amount: Long,
currency: A,
scale: Scale
) {
def toBaseScale: Money[A] = ???
}
and then having a method like:
def +(that: Money[A]): Money[A] = {
Money(
self.toBaseScale.amount + that.toBaseScale.amount,
self.currency,
Scale.Base
)
}
Return type
def +(that: Money): Either[MoneyError, Money] = {
if (that.currency != this.currency) {
Left(CurrencyError("Currencies do not match!"))
else {
Right(
Money(
self.toBaseScale.amount + that.toBaseScale.amount,
self.currency,
Scale.Base
)
)
}
}
It’s just annoying having to deal with Either
everywhere if you know that you’re only dealing with a specific currency.