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.