Low-Level Design · lesson 8 of 18
Beginner
4Intermediate
10- 5 Design a Parking Lot: Allocating and Freeing Spots
- 6 Design an Elevator System: Scheduling Requests
- 7 Design a Library Management System: Borrowing and Returning Books
- 8 Design an Expense-Splitting App: Balances and Minimal Settlements
- 9 Design a URL Shortener: Sequential Codes vs Real Hash Collisions
- 10 Wrapping Objects: Adapter, Facade, and Proxy in Python
- 11 The Composite Pattern in Python: Sizing a File Tree Without isinstance
- 12 The State Pattern in Python: Refactoring a Vending Machine Into State Classes
- 13 Undo and Redo in Python: the Memento and Command Patterns Compared
- 14 Chain of Responsibility in Python: Expense Approvals Without an if/elif Ladder
Advanced
4- 15 Concurrency in LLD: A Real Race Condition in the Parking Lot, Then the Fix
- 16 The Observer Pattern in Practice: A Stock Ticker Notifying Multiple Displays
- 17 Design Patterns That Are Just Language Features: Strategy, Command, Iterator
- 18 Abstract Factory and Prototype in Python: Matching Families and Cloning Objects
Also part of Low-Level Design
12- + SOLID Principles in Java from Java
- + Composition vs Inheritance in Java from Java
- + Design Patterns in Java from Java
- + Python Design Patterns from Python
- + Design Patterns in TypeScript from TypeScript
- + JavaScript Design Patterns from JavaScript
- + Design Patterns in C# from C#
- + Design Patterns in C++ from C++
- + Design Patterns in Go from Go
- + Design Patterns in Rust from Rust
- + Abstract Classes in Java from Java
- + Sealed Classes and Interfaces in Java from Java
Design an Expense-Splitting App: Balances and Minimal Settlements
Three people, two shared expenses, and a greedy algorithm that settles every balance to zero in the fewest possible payments — not one payment per expense.
By SkillByExample · Published
Two expenses among three friends shouldn’t require three separate payments to settle up.
Tracking balances, then settling them
class ExpenseSplitter:
def __init__(self):
self.balances = {} # positive = is owed money, negative = owes money
def add_expense(self, payer, amount, participants):
share = amount / len(participants)
for person in participants:
self.balances[person] = self.balances.get(person, 0) - share
self.balances[payer] = self.balances.get(payer, 0) + amount
print(f"{payer} paid {amount} for {participants} (share: {share:.2f} each)")
def settle(self):
creditors = [(p, b) for p, b in self.balances.items() if b > 0.01]
debtors = [(p, -b) for p, b in self.balances.items() if b < -0.01]
creditors.sort(key=lambda x: -x[1])
debtors.sort(key=lambda x: -x[1])
i = j = 0
transactions = []
while i < len(debtors) and j < len(creditors):
debtor, debt = debtors[i]
creditor, credit = creditors[j]
payment = min(debt, credit)
transactions.append((debtor, creditor, payment))
debtors[i] = (debtor, debt - payment)
creditors[j] = (creditor, credit - payment)
if debtors[i][1] < 0.01:
i += 1
if creditors[j][1] < 0.01:
j += 1
return transactions
splitter = ExpenseSplitter()
splitter.add_expense("alice", 90, ["alice", "bob", "carol"])
splitter.add_expense("bob", 30, ["alice", "bob"])
print("Balances:", {p: round(b, 2) for p, b in splitter.balances.items()})
for debtor, creditor, amount in splitter.settle():
print(f"{debtor} pays {creditor}: {amount:.2f}")
$ python splitter.py
alice paid 90 for ['alice', 'bob', 'carol'] (share: 30.00 each)
bob paid 30 for ['alice', 'bob'] (share: 15.00 each)
Balances: {'alice': 45.0, 'bob': -15.0, 'carol': -30.0}
carol pays alice: 30.00
bob pays alice: 15.00
Two expenses were recorded, but settle() outputs exactly two payments too — in this case
that’s unavoidable (two people owe money, so at least two payments are needed), but with more
people the algorithm often collapses what would naively be one-payment-per-expense into far
fewer transactions, because it settles net balances, not individual expenses.
Practice
1. Confirm balances always sum to (approximately) zero, no matter how many expenses.
class ExpenseSplitter:
def __init__(self):
self.balances = {}
def add_expense(self, payer, amount, participants):
share = amount / len(participants)
for person in participants:
self.balances[person] = self.balances.get(person, 0) - share
self.balances[payer] = self.balances.get(payer, 0) + amount
splitter = ExpenseSplitter()
splitter.add_expense("alice", 90, ["alice", "bob", "carol"])
splitter.add_expense("bob", 30, ["alice", "bob"])
splitter.add_expense("carol", 60, ["alice", "bob", "carol"])
total = sum(splitter.balances.values())
print("Balances:", {p: round(b, 2) for p, b in splitter.balances.items()})
print(f"Sum of all balances: {total:.10f}")
$ python zero_sum.py
Balances: {'alice': 25.0, 'bob': -35.0, 'carol': 10.0}
Sum of all balances: 0.0000000000
Every dollar credited to a payer is debited across the participants in the same call, so the group’s balances always net to zero — this is a useful invariant to assert in a real test suite, not just something to eyeball.
2. Settle a simple two-person expense.
class ExpenseSplitter:
def __init__(self):
self.balances = {}
def add_expense(self, payer, amount, participants):
share = amount / len(participants)
for person in participants:
self.balances[person] = self.balances.get(person, 0) - share
self.balances[payer] = self.balances.get(payer, 0) + amount
def settle(self):
creditors = [(p, b) for p, b in self.balances.items() if b > 0.01]
debtors = [(p, -b) for p, b in self.balances.items() if b < -0.01]
creditors.sort(key=lambda x: -x[1])
debtors.sort(key=lambda x: -x[1])
i = j = 0
transactions = []
while i < len(debtors) and j < len(creditors):
debtor, debt = debtors[i]
creditor, credit = creditors[j]
payment = min(debt, credit)
transactions.append((debtor, creditor, payment))
debtors[i] = (debtor, debt - payment)
creditors[j] = (creditor, credit - payment)
if debtors[i][1] < 0.01:
i += 1
if creditors[j][1] < 0.01:
j += 1
return transactions
splitter = ExpenseSplitter()
splitter.add_expense("alice", 50, ["alice", "bob"])
for debtor, creditor, amount in splitter.settle():
print(f"{debtor} pays {creditor}: {amount:.2f}")
$ python two_person.py
bob pays alice: 25.00
One expense between two people always reduces to exactly one payment: half of 50 back from
whoever didn’t pay.
3. Confirm paying for only yourself nets to a zero balance.
class ExpenseSplitter:
def __init__(self):
self.balances = {}
def add_expense(self, payer, amount, participants):
share = amount / len(participants)
for person in participants:
self.balances[person] = self.balances.get(person, 0) - share
self.balances[payer] = self.balances.get(payer, 0) + amount
print(f"{payer} paid {amount} for {participants}")
splitter = ExpenseSplitter()
splitter.add_expense("alice", 20, ["alice"])
print("Balances:", splitter.balances)
$ python solo_expense.py
alice paid 20 for ['alice']
Balances: {'alice': 0.0}
participants == [payer] means the one deduction and the one credit are the same size and
land on the same key, canceling out exactly — add_expense doesn’t need a special case for
this, the general formula already handles it.
Next: a real race condition in the parking lot from earlier in this track — two threads, one spot, and the bug that appears only under concurrency.