Limited time: AI code review, hints, mock interviews, whiteboard analysis, and all Pro features are unlocked. Enroll
⏱️ 9 min read

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


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:


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

  1. β€œWhy not use double for money?” – 0.1 + 0.2 != 0.3 in IEEE 754. Accumulated rounding over thousands of transactions creates real discrepancies. Financial systems use integers (cents) or arbitrary-precision decimals.

  2. β€œ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.

  3. β€œ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.

  4. β€œ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.

Free system design + DSA prep. If it helped you crack an interview, consider supporting.

SensAI SensAI
Beta
Listening...
Tap mic to stop voice mode

Shape what we build next

Every piece of feedback is read by the team and directly influences our roadmap.

What type of feedback?

Install SystemCraft

Add to your home screen for instant access, offline reading, and a distraction-free experience.

Offline reading Faster loads No browser tabs App-like feel

Unlock AI Features

One click to activate - no payment, no credit card. Just sign in and you're in.

AI code review and hints
SensAI chat assistant
AI mock interviews
Whiteboard analysis
100% free during early access