Extending the Balance Service: Challenges in Implementing Multi-Currency

This post is for Day 15 of Merpay & Mercoin Advent Calendar 2025 , brought to you by @timo from the Merpay Balance team.

We are responsible for the "Balance Service," which manages the ledger and booking of user funds.

In this article, I will introduce the challenges we encountered when extending our system to support multiple currencies and how we resolved them.

Background

Our system runs on a double-entry bookkeeping architecture.

When we originally designed this architecture, we defined the concept of "Exchange Rates" to support future global expansion. However, since the immediate business requirement was domestic, we only implemented the logic for Japanese yen (JPY).

This year, to support the Global Business expansion, we proceeded to implement the full multi-currency feature set. Moving from a JPY-only implementation to a system that handles multiple currencies (USD, EUR) introduced specific engineering issues related to data modeling and precision.

Prerequisites: The "Exchange" Data Model

Before discussing the challenges, it is helpful to understand our transaction structure. We define an Exchange as a single unit containing two distinct flows (Money Out and Money In).

Here is a simplified view of our gRPC Proto definition:

message Exchange {
    // Header Level: Metadata
    string transaction_id = 1;

    // Detail Level: The Legs of the transaction
    Source source = 2; // Who is paying? (e.g., TWD)
    Target target = 3; // Who is receiving? (e.g., JPY)
}

Ideally, the core logic of our system is maintaining the balance: Source Amount * Exchange Rate ≈ Target Amount. Managing this equation and deciding where to store the rate became the central theme of our challenges.

Challenge 1: Data Modeling for Exchange Rates

An "Exchange" transaction consists of a Source (Money Out) and a Target (Money In). The first issue we faced was determining where to save the exchange rate.

We considered two storage patterns:

Option A – Exchange Level (Header): This approach places the rate in the Exchange table and the API Header.

This fit our current use cases perfectly and was easy to implement. However, it was not flexible. If we ever needed to support mixed-currency payments (e.g., TWD + USD) in the future, this structure would require a difficult migration.

Option B – Source Level (Detail): This approach places the rate in the Source table and the API Source message.

It is highly flexible and easy to extend. However, for our current needs, it felt like over-engineering. Forcing the Proto to carry a rate for every single source—when we currently only do 1-to-1 exchanges—would make the API unnecessarily complicated for our clients.

Our Decision: We decided to separate the API design from the database schema.

  • API Level (Proto): We accept the rate in the Exchange Level. This keeps the integration simple for our upstream clients, who simply request: "Convert TWD to JPY at rate 4.2."
  • Database Level: We map and store the rate at the Source Level.

This hybrid approach gives us simplicity in the API and future flexibility in the database. When the time comes to support multi-source payments, our database will be ready without migration, even though our API is currently optimized for simple use cases.
multiple-currency-and-exchange-rate

Challenge 2: Precision and Validation Logic

The second issue was validation logic. In a JPY environment, the math is always integer-based (100 = 100). In a multi-currency environment, Source * Rate results in decimals.

The problem is that we cannot assume which rounding method (Floor, Ceiling, etc.) the clients use. If we enforce a strict rule (e.g., Round-Half-Up), valid transactions might fail due to minor rounding differences.

To solve this, we first clarified the responsibility of the Balance Service. It is a System of Record, not a Pricing Engine.

If the upstream service rounds down and "loses" a fraction of the value, that is a business decision made upstream. Our responsibility is not to enforce pricing strategy, but to ensure the booking is mathematically consistent within a reasonable margin of error.

The Solution: Based on this core principle, we implemented a Flexible Validation approach. Instead of checking for an exact match, we check if the Target Amount is "mathematically reasonable" given the Source and Rate.

  1. Calculate: Compute Source * Rate.
  2. Determine Precision: Compare the calculated result with the requested Target Amount. The value with fewer decimal places is used as the reference.
  3. Validate: Round the precise value both Up and Down. If either result matches the reference, the request is accepted.

This allows us to accept valid transactions regardless of the upstream rounding method (Floor, Ceiling, Banker’s Rounding) while still blocking truly incorrect rates.
example-of-validation-logic-of-multi-currency-transaction

Challenge 3: Designing the Reversal Interface

The third point is a design insight regarding our existing Reversal API.

We already had a stable Reversal endpoint. With the introduction of multi-currency support, we faced a question: Should we add an exchange_rate field to the Reversal request?

Ideally, a Reversal operation should only output the original exchange rate (for reference), never take it as input.

If we added an exchange_rate field to the input, it would create confusion for the client: "Should I send the current market rate or the original one?" If they accidentally sent the current rate, the ledger would become unbalanced.

Our Approach: We decided to use exchange_rate as output-only in the Reversal API. The system internally looks up the original rate to ensure the "Undo" is mathematically exact. By limiting the input schema, we prevented the possibility of rate fluctuations errors by design.

It is worth noting that, if a Business Refund is required (where the refund is based on the current market rate), this does not need a special endpoint. It can be implemented simply by calling the Exchange endpoint, swapping the original Source and Target, and providing the new rate.

Future Issues: The Attribution Problem

Looking ahead, we anticipate complexity with Multi-Source Payments. If a transaction uses multiple sources (e.g., TWD and USD) to pay a single JPY target, rounding errors may cause the sum of the converted amounts to differ from the total target amount. Determining how to assign this rounding difference (which source absorbs the gap) to maintain a balanced ledger is a topic we recognize as a future challenge that we will need to solve.

Extra: The Regional Constraint

Finally, I want to touch upon a design requirement that often comes up during global expansion.

In the initial design phase, adding a Currency field seems like the only requirement. However, a critical realization often follows: Currency and Region have a many-to-many relationship.

A single currency code does not uniquely identify the legal region. For example, USD is the official currency of the United States, but it is also used in other places (like Ecuador). Conversely, a single region like Panama uses both PAB and USD as official currencies. (List of circulating currencies)

Legally, "Region A-held USD" is different from "Region B-held USD" due to different financial rules. Since the currency code is the same, we cannot tell them apart by Currency alone.

Therefore, we established Region as a required dimension in our ledger. By managing assets based on the combination of Region + Currency, we ensure that funds remain correctly separated across different regions.

Conclusion

Supporting multiple currencies required more than just adding a currency field. We had to finalize the data model, implement flexible validation for decimals, and strictly define the scope of reversals.

By addressing these specific challenges, we were able to support global business requirements by simply extending our existing architecture, without the need for a fundamental redesign.

In this post, I shared how we extended the Balance Service and the solutions we used to handle multi-currency challenges. I hope this gives you some new ideas for your own system designs! 🙂

Tomorrow’s article will be by @Stefan_droid. Look forward to it!

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加