Skip to Content
Course content

250: Practice Exercise: Modeling a Multi-Currency Wallet System

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've been thinking about how we'd actually implement a multi-currency wallet. On the surface, it seems trivial—just a list of numbers, right? But as soon as you start coding it, you realize how quickly things can get messy if you don't lean into Scala's type system. Let's try to build this together, starting with the most naive approach and seeing where it breaks.

The "Quick and Dirty" Approach

My first instinct is usually just to use a Map[String, Double]. The string is the currency code (like "USD"), and the double is the amount. Let's see how that looks:

case class Wallet(balances: Map[String, Double]) {
  def addFunds(currency: String, amount: Double): Wallet = {
    val currentBalance = balances.getOrElse(currency, 0.0)
    Wallet(balances + (currency -> (currentBalance + amount)))
  }
}

I can run this, and it works. I can add "USD" or "EUR". But I already hate it. Why? Because I can pass "Potato" as a currency and the compiler won't blink. Also, using Double for money is a cardinal sin in software engineering because of floating-point precision errors. If I'm moving millions of dollars, those tiny rounding errors become very real legal problems.

Tightening the Screws with ADTs

I want the compiler to stop me from using fake currencies. I'll switch to a sealed trait. This way, I can define exactly which currencies our system supports. I'll also swap Double for BigDecimal to keep the decimals precise.

sealed trait Currency
case object USD extends Currency
case object EUR extends Currency
case object BTC extends Currency

case class Wallet(balances: Map[Currency, BigDecimal]) {
  def addFunds(currency: Currency, amount: BigDecimal): Wallet = {
    val currentBalance = balances.getOrElse(currency, BigDecimal(0))
    Wallet(balances + (currency -> (currentBalance + amount)))
  }
}

This feels much safer. If I try to pass a random string now, it's a compile-time error. Now, let's think about spending. I want to subtract an amount, but I can't let the balance go negative.

Wrestling with the Balance Map

If I just subtract the value, I might end up with -50 USD. I need a way to signal that the transaction failed. I could throw an exception, but that's lazy. Let's use Either. It's much more idiomatic in Scala to return the error as a value.

def spendFunds(currency: Currency, amount: BigDecimal): Either[String, Wallet] = {
  val currentBalance = balances.getOrElse(currency, BigDecimal(0))
  if (currentBalance >= amount) {
    Right(Wallet(balances + (currency -> (currentBalance - amount))))
  } else {
    Left(s"Insufficient funds in $currency")
  }
}

Wait, I just noticed something. If the balance for a currency hits exactly zero, the map still keeps that entry. It's not a huge deal, but it's cluttered. I can use filter or a conditional to remove the key if the value is zero. Actually, let's keep it simple for now and focus on the logic. The real challenge is: what if I want to see my total value in a single "base" currency?

Handling Exchange Rates

To do a total valuation, I can't just sum the map because you can't add 1 BTC to 1 USD and get 2 of "something". I need an exchange rate provider. I'll model this as a simple function that takes two currencies and returns a rate.

type ExchangeRateProvider = (Currency, Currency) => BigDecimal

def calculateTotalValue(base: Currency, rates: ExchangeRateProvider): BigDecimal = {
  balances.foldLeft(BigDecimal(0)) { case (acc, (currency, amount)) =>
    val rate = rates(currency, base)
    acc + (amount * rate)
  }
}

I just realized a potential bug here: what if the ExchangeRateProvider doesn't have a rate for a specific pair? Right now, my type signature assumes it always returns a BigDecimal. In a real system, that function should probably return an Option[BigDecimal]. If I can't find a rate, the whole valuation should probably fail. It's better to be explicitly "Unknown" than to accidentally assume a rate of 0.0 and tell a user they are broke when they actually have a lot of Bitcoin.




📋 Practical Task

Exercise: Implementing the Cross-Currency Transfer

You have the basic Wallet system. Your task is to implement a new method called transferFunds. This method should allow a user to move value from one currency to another within the same wallet, using a provided exchange rate.

Requirements:

  • The method signature should be: def transferFunds(from: Currency, to: Currency, amount: BigDecimal, rate: BigDecimal): Either[String, Wallet].
  • The amount is the amount being taken out of the from currency.
  • The amount added to the to currency should be amount * rate.
  • The transfer must fail (return a Left) if the wallet has insufficient funds in the from currency.
  • The method must return a new Wallet instance with both balances updated if the transfer is successful.

Starter Code:

sealed trait Currency
case object USD extends Currency
case object EUR extends Currency
case object BTC extends Currency

case class Wallet(balances: Map[Currency, BigDecimal]) {
  // Your transferFunds implementation goes here
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.