Money Handling
Money is not a float. Using double for currency leads to silent rounding errors that accumulate into real financial discrepancies. Any LLD problem involving payments, billing, wallets, or expense splitting needs proper money representation.
Why this matters: Splitwise, payment wallets, e-commerce carts, parking billing β many interview problems involve money. Using double balance = 100.0 is a red flag. Interviewers notice when you handle money correctly.
Prerequisites
- Exception Handling β Money operations need proper error handling
- SOLID Principles β Money is a value object (SRP)
The Problem with Floating Point
flowchart LR
A["0.1 + 0.2 = ?"]:::client
B["Expected: 0.3"]:::service
C["Actual: 0.30000000000000004"]:::data
A --> B
A --> C
classDef client fill:#4c3a5e,stroke:#818cf8,color:#e2e8f0
classDef service fill:#1a3a2a,stroke:#4ade80,color:#e2e8f0
classDef data fill:#3b3520,stroke:#fbbf24,color:#e2e8f0
This isnβt just academic. Real consequences:
- Split $10 three ways:
10.0 / 3 = 3.3333...β who gets the missing penny? - Sum 1000 transactions: accumulated rounding errors can be off by dollars
- Compare totals:
total == expectedfails due to floating point imprecision
Solution 1: Minor Units (Store Cents as Integers)
The simplest approach: represent all amounts in the smallest currency unit (cents for USD, paise for INR). No fractions, no rounding issues.
// Money as a value object using minor units (cents)
public final class Money implements Comparable<Money> {
private final long amountInCents;
private final Currency currency;
private Money(long amountInCents, Currency currency) {
this.amountInCents = amountInCents;
this.currency = currency;
}
// Factory methods
public static Money ofCents(long cents, Currency currency) {
return new Money(cents, currency);
}
public static Money of(double majorUnits, Currency currency) {
return new Money(Math.round(majorUnits * 100), currency);
}
public static Money rupees(double amount) {
return of(amount, Currency.INR);
}
public static Money zero(Currency currency) {
return new Money(0, currency);
}
// Arithmetic -- always returns new Money (immutable)
public Money add(Money other) {
assertSameCurrency(other);
return new Money(this.amountInCents + other.amountInCents, currency);
}
public Money subtract(Money other) {
assertSameCurrency(other);
return new Money(this.amountInCents - other.amountInCents, currency);
}
public Money multiply(int factor) {
return new Money(this.amountInCents * factor, currency);
}
// Split: handles the "penny problem" -- remainder distributed to first N shares
public List<Money> split(int ways) {
if (ways <= 0) throw new IllegalArgumentException("Cannot split into " + ways);
long base = amountInCents / ways;
long remainder = amountInCents % ways;
List<Money> shares = new ArrayList<>(ways);
for (int i = 0; i < ways; i++) {
long share = base + (i < remainder ? 1 : 0);
shares.add(new Money(share, currency));
}
return shares; // Sum of shares == original. Always.
}
// Comparisons
public boolean isGreaterThan(Money other) {
assertSameCurrency(other);
return amountInCents > other.amountInCents;
}
public boolean isNegative() { return amountInCents < 0; }
public boolean isZero() { return amountInCents == 0; }
@Override
public int compareTo(Money other) {
assertSameCurrency(other);
return Long.compare(amountInCents, other.amountInCents);
}
// Display
public String toDisplayString() {
long major = amountInCents / 100;
long minor = Math.abs(amountInCents % 100);
return String.format("%s %d.%02d", currency.getSymbol(), major, minor);
}
private void assertSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new CurrencyMismatchException(this.currency, other.currency);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money m)) return false;
return amountInCents == m.amountInCents && currency.equals(m.currency);
}
@Override
public int hashCode() { return Objects.hash(amountInCents, currency); }
@Override
public String toString() { return toDisplayString(); }
}
from dataclasses import dataclass
from enum import Enum
class Currency(Enum):
USD = ("$", 2)
INR = ("Rs", 2)
JPY = ("Yen", 0) # Yen has no minor unit
def __init__(self, symbol: str, decimal_places: int):
self.symbol = symbol
self.decimal_places = decimal_places
@dataclass(frozen=True) # Immutable value object
class Money:
amount_minor: int # cents/paise
currency: Currency
@classmethod
def of(cls, major_units: float, currency: Currency) -> 'Money':
minor = round(major_units * (10 ** currency.decimal_places))
return cls(minor, currency)
@classmethod
def zero(cls, currency: Currency) -> 'Money':
return cls(0, currency)
def add(self, other: 'Money') -> 'Money':
self._assert_same_currency(other)
return Money(self.amount_minor + other.amount_minor, self.currency)
def subtract(self, other: 'Money') -> 'Money':
self._assert_same_currency(other)
return Money(self.amount_minor - other.amount_minor, self.currency)
def multiply(self, factor: int) -> 'Money':
return Money(self.amount_minor * factor, self.currency)
def split(self, ways: int) -> list['Money']:
"""Split fairly -- remainder goes to first N people."""
base = self.amount_minor // ways
remainder = self.amount_minor % ways
return [
Money(base + (1 if i < remainder else 0), self.currency)
for i in range(ways)
]
def is_negative(self) -> bool:
return self.amount_minor < 0
def is_greater_than(self, other: 'Money') -> bool:
self._assert_same_currency(other)
return self.amount_minor > other.amount_minor
def display(self) -> str:
places = self.currency.decimal_places
divisor = 10 ** places
major = self.amount_minor // divisor
minor = abs(self.amount_minor % divisor)
return f"{self.currency.symbol} {major}.{str(minor).zfill(places)}"
def _assert_same_currency(self, other: 'Money'):
if self.currency != other.currency:
raise CurrencyMismatchError(self.currency, other.currency)
# Usage
bill = Money.of(100.50, Currency.INR)
shares = bill.split(3) # [Rs 33.50, Rs 33.50, Rs 33.50] -- sums to Rs 100.50
class Money {
long amountCents_;
Currency currency_;
public:
Money(long cents, Currency curr) : amountCents_(cents), currency_(curr) {}
static Money ofMajor(double major, Currency curr) {
return Money(static_cast<long>(round(major * 100)), curr);
}
static Money zero(Currency curr) { return Money(0, curr); }
Money add(const Money& other) const {
assertSameCurrency(other);
return Money(amountCents_ + other.amountCents_, currency_);
}
Money subtract(const Money& other) const {
assertSameCurrency(other);
return Money(amountCents_ - other.amountCents_, currency_);
}
vector<Money> split(int ways) const {
long base = amountCents_ / ways;
long remainder = amountCents_ % ways;
vector<Money> shares;
for (int i = 0; i < ways; ++i) {
shares.emplace_back(base + (i < remainder ? 1 : 0), currency_);
}
return shares;
}
bool isNegative() const { return amountCents_ < 0; }
bool isGreaterThan(const Money& other) const {
return amountCents_ > other.amountCents_;
}
bool operator==(const Money& other) const {
return amountCents_ == other.amountCents_ && currency_ == other.currency_;
}
private:
void assertSameCurrency(const Money& other) const {
if (currency_ != other.currency_)
throw CurrencyMismatchException(currency_, other.currency_);
}
};
Solution 2: BigDecimal (When Precision Matters)
For complex financial calculations (compound interest, tax calculations), use arbitrary-precision decimals.
// Java BigDecimal: explicit scale and rounding
BigDecimal price = new BigDecimal("19.99"); // Use String constructor, not double!
BigDecimal tax = price.multiply(new BigDecimal("0.18"))
.setScale(2, RoundingMode.HALF_UP); // Rs 3.60
BigDecimal total = price.add(tax); // Rs 23.59
// Splitting with BigDecimal
BigDecimal splitAmount = total.divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP);
from decimal import Decimal, ROUND_HALF_UP
# Python Decimal: use string initialization
price = Decimal("19.99") # NOT Decimal(19.99) -- that inherits float imprecision
tax = (price * Decimal("0.18")).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
total = price + tax
# Splitting
share = (total / 3).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
// C++ doesn't have built-in BigDecimal
// Option 1: Use long/int64_t for cents (recommended for interviews)
// Option 2: Third-party library (Boost.Multiprecision)
// Option 3: Custom decimal class wrapping int64_t
// For interviews, minor units (cents as int64_t) is the practical choice
int64_t priceInCents = 1999; // $19.99
int64_t taxInCents = static_cast<int64_t>(round(priceInCents * 0.18)); // 360
int64_t totalInCents = priceInCents + taxInCents; // 2359
The Ledger Pattern
For wallets and accounts, donβt just store a balance. Store an append-only ledger of transactions and derive the balance from it. This gives you auditability and makes reconciliation possible.
public class Ledger {
private final List<LedgerEntry> entries = new ArrayList<>();
private Money balance;
private final Currency currency;
public Ledger(Currency currency) {
this.currency = currency;
this.balance = Money.zero(currency);
}
public void credit(Money amount, String description, String referenceId) {
LedgerEntry entry = new LedgerEntry(
EntryType.CREDIT, amount, description, referenceId, Instant.now()
);
entries.add(entry);
balance = balance.add(amount);
}
public void debit(Money amount, String description, String referenceId) {
if (balance.isLessThan(amount)) {
throw new InsufficientBalanceException(balance, amount);
}
LedgerEntry entry = new LedgerEntry(
EntryType.DEBIT, amount, description, referenceId, Instant.now()
);
entries.add(entry);
balance = balance.subtract(amount);
}
public Money getBalance() { return balance; }
// Reconciliation: recompute balance from entries
public Money reconcile() {
Money computed = Money.zero(currency);
for (LedgerEntry entry : entries) {
if (entry.getType() == EntryType.CREDIT) {
computed = computed.add(entry.getAmount());
} else {
computed = computed.subtract(entry.getAmount());
}
}
return computed; // Should equal balance
}
}
class Ledger:
def __init__(self, currency: Currency):
self._entries: list[LedgerEntry] = []
self._balance = Money.zero(currency)
self._currency = currency
def credit(self, amount: Money, description: str, ref_id: str):
entry = LedgerEntry(EntryType.CREDIT, amount, description, ref_id)
self._entries.append(entry)
self._balance = self._balance.add(amount)
def debit(self, amount: Money, description: str, ref_id: str):
if self._balance.amount_minor < amount.amount_minor:
raise InsufficientBalanceError()
entry = LedgerEntry(EntryType.DEBIT, amount, description, ref_id)
self._entries.append(entry)
self._balance = self._balance.subtract(amount)
@property
def balance(self) -> Money:
return self._balance
def reconcile(self) -> Money:
computed = Money.zero(self._currency)
for entry in self._entries:
if entry.type == EntryType.CREDIT:
computed = computed.add(entry.amount)
else:
computed = computed.subtract(entry.amount)
return computed
class Ledger {
vector<LedgerEntry> entries_;
Money balance_;
Currency currency_;
public:
explicit Ledger(Currency curr) : currency_(curr), balance_(Money::zero(curr)) {}
void credit(Money amount, const string& desc, const string& refId) {
entries_.emplace_back(EntryType::CREDIT, amount, desc, refId);
balance_ = balance_.add(amount);
}
void debit(Money amount, const string& desc, const string& refId) {
if (balance_.amountCents() < amount.amountCents())
throw InsufficientBalanceException();
entries_.emplace_back(EntryType::DEBIT, amount, desc, refId);
balance_ = balance_.subtract(amount);
}
Money balance() const { return balance_; }
};
When to Use vs When to Avoid
| Approach | Use When | Complexity |
|---|---|---|
| Minor units (cents as long) | Most LLD problems, simple arithmetic | Low |
| BigDecimal | Tax calculations, compound interest, currency conversion | Medium |
| Ledger pattern | Wallets, accounts, anywhere auditability matters | Medium |
| Double/float | Never for money (OK for display approximation only) | N/A |
Interview Questions
-
βWhy not use double for money?β β
0.1 + 0.2 != 0.3in IEEE 754. Accumulated rounding over thousands of transactions creates real discrepancies. Financial systems use integers (cents) or arbitrary-precision decimals. -
βHow do you split $10 among 3 people?β β 1000 cents / 3 = 333 per person, remainder 1. First person gets 334, others get 333. Total: 1000. No money lost, no money created.
-
βWhatβs the advantage of a ledger over just storing a balance?β β Auditability. You can trace every change, detect anomalies, reconcile balances, and replay history. Essential for anything financial.
-
βHow do you handle multiple currencies?β β Never mix currencies in arithmetic. The Money class should enforce same-currency checks. Currency conversion is a separate service with explicit rates.
See It in Action
Money handling is critical in Payment Wallet, Splitwise, and E-commerce Cart.