cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 188 of 218
Ideas this builds on
Low-level design188 / 218

Design an expense ledger

Picture

Split 100 minor units among Ana, Bo, and Cy as 34,33,33 using a documented remainder rule. If Ana pays, balances become +66,−33,−33. Their sum stays zero.

Predict

What information do net balances lose compared with the original expense record?

Walk through the small example

They keep how much each participant should receive or pay overall, but not the original pairwise obligations or why a charge occurred. Retain records for explanation and audit.

Mental model and complete Python solution

Use integer minor units in one currency. equal_shares divides exactly and distributes the remainder to the earliest participants in the supplied order. add validates the complete split before computing replacement balances, then records the expense. A payer may be outside the participant list, meaning they funded others’ shares. Positive balance is receivable, negative is payable. This is a netting ledger, not a bank ledger or a currency-conversion system.

What the function promises: Expenses and shares are nonnegative integers, participant names are nonempty and distinct, and shares must total the expense. The ledger uses one caller-declared currency. It stores original expense records and net balances.

def equal_shares(amount, people):
    if (type(amount) is not int or amount < 0 or not people
            or len(set(people)) != len(people) or any(not p for p in people)):
        raise ValueError("Invalid amount or participants")
    base, remainder = divmod(amount, len(people))
    return {person: base + (i < remainder) for i, person in enumerate(people)}

class ExpenseLedger:
    def __init__(self, currency):
        self.currency = currency
        self.balances = {}
        self.records = []

    def add(self, payer, amount, shares):
        if (not payer or type(amount) is not int or amount < 0 or not shares
                or any(not p or type(value) is not int or value < 0
                       for p, value in shares.items())
                or sum(shares.values()) != amount):
            raise ValueError("Shares must exactly match the expense")
        next_balances = self.balances.copy()
        next_balances[payer] = next_balances.get(payer, 0) + amount
        for person, share in shares.items():
            next_balances[person] = next_balances.get(person, 0) - share
        self.balances = next_balances
        self.records.append((payer, amount, dict(shares)))

ledger = ExpenseLedger("INR")
shares = equal_shares(100, ["Ana", "Bo", "Cy"])
assert shares == {"Ana": 34, "Bo": 33, "Cy": 33}
ledger.add("Ana", 100, shares)
assert ledger.balances == {"Ana": 66, "Bo": -33, "Cy": -33}
assert sum(ledger.balances.values()) == 0
before = ledger.balances.copy()
try:
    ledger.add("Bo", 50, {"Ana": 49})
except ValueError:
    pass
else:
    raise AssertionError("Unbalanced expense accepted")
assert ledger.balances == before and len(ledger.records) == 1

Work and storage: For U known users and P shares, add takes O(U+P) time because it copies balances before committing. Equal splitting is O(P). Records grow with historical expense data.

When to recognize it: Money moves between balances; the sum of all balances must remain zero.

Check before applying it: Net balances do not preserve who originally owed whom. Keep expense records for audit; do not use binary floats for exact currency units.

Change one rule

A retried API request submits the same expense twice. Does conservation catch the duplication?

See what changes

No. Both duplicate entries can still sum to zero. Give expenses stable IDs and reject or replay duplicates. Invariants such as conservation and idempotency protect different failure modes.

Connect

invariants: Conservation is a rule every operation must preserve.

Make failure and retries part of the design: Reject invalid splits before changing any balance.