Designing conversions as linked financial events instead of balance edits.
Adding a currency to a wallet product is not simply adding another currency code to a transaction table.
Each user can own multiple balances, every currency has different decimal rules, and conversions affect two wallets at once. Historical transactions must retain the rate used at execution, even after market or configured rates change.
Give every user-currency pair one wallet
A database constraint should ensure that a user has no more than one active wallet for a supported currency.
UniqueConstraint(fields=["user", "currency"])
The currency itself should come from controlled configuration containing its code, precision, funding capability, withdrawal capability, display symbol, and activation state.
Treat conversion as a financial transaction
A conversion should create an immutable record containing:
- one reference;
- source currency and amount;
- destination currency and amount;
- applied exchange rate;
- rate source;
- relevant country or market context; and
- execution time.
Inside one database transaction, lock the source and destination wallets in a consistent order, debit the source, credit the destination, and write both ledger effects.
Consistent lock ordering reduces deadlock risk when concurrent conversions involve the same currency pair in opposite directions.
Snapshot the applied rate
Never reconstruct a completed conversion using the current exchange rate. The historical record must preserve the rate that produced the destination amount.
If staff can configure an override rate, preserve an audit history showing the previous value, new value, actor, timestamp, and reason. Existing conversions remain unchanged.
Use decimal arithmetic deliberately
Floating-point arithmetic is inappropriate for wallet balances. Use decimal values and define rounding at the business boundary:
- wallet storage precision;
- quote display precision;
- provider submission precision; and
- ledger settlement precision.
Round once at the required boundary, then persist the value used. Repeated rounding at multiple layers creates unexplained differences.
Keep the legacy interface separate
When migrating from one balance field to multi-currency wallets, older clients may still expect a primary account balance. A compatibility property can expose the designated primary wallet without making the legacy column authoritative again.
Migration code should be explicit, reversible where practical, and followed by constraints that prevent the old and new systems from drifting.
The design principle
A multi-currency wallet is a collection of currency-specific ledgers connected by immutable conversion records.
The rate, rounding decision, and two wallet effects are part of one financial event. Preserving them together keeps historical balances explainable as currencies and pricing rules evolve.
Top comments (0)