Currency conversion is easy. Knowing which rate applied to which transaction three weeks later — that's the hard part.
Most payment systems start with a single currency. You process payments in one denomination, your bank accounts are in that denomination, and your ledger is straightforward. Then someone says "we need to handle USD and EUR" and suddenly you have a different kind of problem.
The conversion itself is trivial — multiply by a rate. But the downstream effects touch everything: your ledger structure, your reconciliation logic, your reporting, your cash position calculations, and your audit trail. Get the architecture wrong here and you'll spend months chasing phantom discrepancies that turn out to be rounding differences compounded across thousands of transactions.
In a multi-currency payment flow, money changes denomination at some point between the sender and the receiver. The question is: what exchange rate was used, when was it locked, and where is that decision recorded?
This sounds like a simple data point. It's not. Consider a typical flow:
Between step 1 and step 3, there are timing gaps. The conversion might happen on a different day. The rate might be a market rate, a negotiated rate, or an internal transfer rate with a margin baked in. The same pool of USD might fund multiple EUR payouts at different rates on different days.
If your system treats the exchange rate as a single field on a transaction record, you'll be fine for a while. Then month-end comes around, and your finance team asks: "What rate did we actually get on the €45M we paid out last Tuesday?" And the answer will be "it's complicated" — which is not what they want to hear.
The simplest approach — and often the right one — is to treat currency conversion as a boundary event. Money enters your system in one currency, gets converted, and from that point forward everything operates in the local currency.
Your internal ledger, your reconciliation, your operational dashboards — all in a single denomination. The conversion event is logged with the rate, the timestamp, and the amounts on both sides. But operationally, you're running a single-currency system.
This works when:
This doesn't work when:
When you genuinely operate in multiple currencies — holding balances, making payments, and reporting in more than one denomination — you need a dual-entry approach.
Every conversion creates two ledger entries: a debit in the source currency and a credit in the target currency, linked by a conversion record that captures the rate. Your balance sheet has currency-specific accounts. Your cash position report shows balances per currency.
The conversion ledger is its own entity — it tracks every rate applied, the volume converted, the margin (if applicable), and the settlement timing. This is where your finance team goes when they need to answer "what rate did we get."
This is more complex to build and maintain, but it gives you:
This is the subtle one. When a payment is instructed, it might use one rate. When it actually settles at the bank, the effective rate might be different — especially if the bank applies its own spread, or if the payment settles the next business day and the rate has moved.
Your architecture needs to decide: do you record the rate at instruction time, at settlement time, or both?
The answer is both. Always both.
The instruction rate is what your system promised. The settlement rate is what actually happened. The difference between them is either margin, slippage, or a data quality issue — and you need to know which.
Systems that only record one rate end up with reconciliation gaps they can never fully explain. "We expected to pay €15.2M but the bank shows €15.3M" — is that an error, a rate movement, or a bank fee? Without both rates on record, you're guessing.
Currency conversion introduces rounding at every step. Multiply 1,247 transactions by a rate with six decimal places, round each one to two decimal places, and sum them up. Now multiply the total by the same rate and round once. The two numbers will be different.
This is not a bug. It's arithmetic. But it means your reconciliation will always show small discrepancies unless you design for it.
The pragmatic approach: define a rounding tolerance per currency pair, document it, and auto-clear discrepancies below the threshold. Track them — they should be small and predictable. If they're not, something else is wrong.
The mistake is trying to eliminate rounding discrepancies entirely. You can't. You can only make them small, predictable, and documented.
Don't mix currencies in the same ledger account. Ever. One account, one currency. If you need a consolidated view, build it as a report layer on top.
Store rates with enough precision. Six decimal places minimum. Don't truncate for display convenience and accidentally persist the truncated value.
Record the rate source. Was it a market rate from a feed? A manual entry? A negotiated rate from a contract? This matters for auditing.
Reconcile per currency, not across currencies. Your EUR bank statement should reconcile against your EUR ledger. Converting everything to a "base currency" for reconciliation just moves the rounding problem to a different place.
Build the FX view last. Get your single-currency operations working correctly first. The multi-currency layer is an addition, not a foundation. Teams that start with multi-currency often drown in complexity before they process their first transaction.
Zenlime builds payment and treasury systems for financial services companies operating across currencies. Start a conversation.