MindForge
MindForge
DashboardHelp & Support
Dashboard
MindForge

Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Design an Expense-Sharing App (Splitwise)
Course contents

Interview Preparation Bootcamp

Course progress

0%
0 of 177 modules complete
Start Here0/1
  • How to Use This Course
Data Structures & Algorithms0/27
  • Arrays and Hashing
  • Two Pointers
  • Sliding Window
  • Sliding Window: Harder Problems
  • Binary Search
  • Stacks
  • Practice Test: Arrays, Pointers, Windows, Search and Stacks
  • Trees: Traversals and Core Patterns
  • Binary Search Trees and Rebuilding a Tree
  • Tries (Prefix Trees)
  • Heaps and Priority Queues
  • Graphs: BFS and DFS
  • Topological Sort and Cycle Detection
  • Union-Find (Disjoint Set Union)
  • Practice Test: Trees, Heaps and Graphs
  • Dynamic Programming Basics
  • Dynamic Programming: Two Dimensions
  • Dynamic Programming: Hard String Matching
  • Backtracking
  • Backtracking: Constraint Satisfaction
  • Greedy Algorithms
  • Interval Problems
  • Bit Manipulation
  • Math and Geometry
  • String Manipulation
  • Pattern Recall and Mock Coding Rounds
  • Practice Test: DP, Backtracking, Greedy and More

System Design

Foundations0/16
  • The System Design Interview Framework
  • Back-of-the-Envelope Estimation
  • Networking and Protocols for Design
  • API Design and Idempotency
  • Load Balancing, Proxies, and Scaling Out
  • Caching and CDNs
  • Choosing a Database: Storage, Indexes and Transactions
  • Replication, Partitioning, and Sharding
  • CAP, PACELC, and Levels of Consistency
  • Messaging, Queues, and Event-Driven Architecture
  • Distributed Transactions and Data Integrity
  • Coordination, Consensus, and Distributed Locks
  • Reliability, Resilience, and Rate Limiting
  • Observability, Security, and Cost
  • System Design Cheat Sheet
  • Practice Test: System Design Foundations
Case Studies0/23
  • Design a URL Shortener
  • Design a Rate Limiter
  • Design a Notification Service
  • Design a Job Queue System
  • Design a Chat App (WhatsApp)
  • Design an Analytics Pipeline
  • Design a Multi-tenant SaaS Architecture
  • Design a Social Media Feed (Twitter/X)
  • Design a Search Autocomplete System
  • Design a File Storage Service (Dropbox)
  • Design Google Docs
  • Design a Code Review System
  • Design Netflix
  • Design YouTube
  • Design Spotify
  • Design Uber/Lyft
  • Design Airbnb
  • Design Ticketmaster (Eventbrite)
  • Design an E-commerce Cart, Checkout, and Catalog
  • Design a Payment System
  • Design a Logging and Monitoring System
  • Mock Design Rounds
  • Practice Test: System Design Case Studies
Low-Level Design0/19
  • The LLD Interview Framework and UML
  • OOP Foundations for Design
  • SOLID Principles
  • Creational Patterns
  • Structural Patterns
  • Behavioral Patterns I — Strategy, Observer, Command, State, Template Method
  • Behavioral Patterns II and Anti-Patterns
  • Concurrency in Low-Level Design
  • Design a Parking Lot
  • Design an Elevator System
  • Design a Movie Ticket Booking System
  • Design an Expense-Sharing App (Splitwise)
  • Design a Vending Machine and an ATM
  • Design a Rate Limiter and a Logging Framework
  • Design a Notification Service and an In-Memory Cache
  • Design Games — Tic-Tac-Toe, Snake and Ladder, Chess
  • Database Design for LLD
  • LLD Cheat Sheet and Recall Drill
  • Practice Test: Low-Level Design

Backend

Python0/5
  • How Python Runs Your Code
  • Functions Deep Dive: Closures, Generators, Context Managers, Scope
  • Classes Deep Dive: Method Types, MRO, Metaclasses
  • Standard Library and Serialization
  • Practice Test: Python
Django0/6
  • Django Request Lifecycle
  • Lab: Django Middleware — Request ID & Response Time
  • Django ORM and Querysets
  • Django's Cache Framework
  • Practice Test: Django
  • Django in Production: Debugging Labs
FastAPI0/5
  • FastAPI, ASGI, and Async
  • FastAPI Dependency Injection
  • FastAPI Background Tasks
  • Lab: FastAPI Async — Concurrent Aggregation
  • Practice Test: FastAPI
Databases (PostgreSQL)0/6
  • Indexing
  • Query Optimization
  • Transactions and Isolation
  • Migrations and Schema Changes
  • Hierarchical Data and Safe Concurrent Updates
  • Practice Test: Databases
Caching, Queues & Distributed Systems0/8
  • Redis Caching
  • Redis Advanced Patterns
  • Celery and Background Workers
  • Kafka and Message Queues
  • Async Pipelines
  • Distributed Systems Basics
  • Distributed Locks
  • Practice Test: Caching, Queues and Distributed Systems
APIs, Security & Deployment0/9
  • REST API Design
  • API Versioning and Error Handling
  • API Security and Authentication
  • WebSockets and Real-Time Systems
  • Docker and Containers
  • File Storage with S3
  • Testing Strategies
  • Mock Backend Round
  • Practice Test: APIs, Security and Deployment
Frontend0/38
  • HTML Document Structure and Semantics
  • CSS Box Model, Flexbox and Grid
  • CSS Architecture and Theming
  • CSS and Rendering Performance
  • Variables, Scope, Hoisting, and Truthiness
  • Functions, this, and Binding
  • Objects and Prototypal Inheritance
  • JavaScript Utility Patterns: Cloning, Caching, and Memory
  • Promises From Scratch
  • The Event Loop and Browser Rendering Pipeline
  • The DOM: Nodes, Events, and Traversal
  • Practice Test: HTML, CSS and JavaScript
  • React Rendering Fundamentals
  • React Hooks Internals
  • React Performance
  • Lab: React — Stateful Counter Component
  • State Management
  • State Machines in React
  • React Custom Hooks and Composition Patterns
  • TypeScript for React
  • React Testing
  • Practice Test: React
  • HTTP and Caching
  • Network Performance and Web Vitals
  • Code Splitting and Bundles
  • Virtualization
  • GraphQL and Data Fetching
  • Web Security
  • WebSockets and Real-time
  • SSR and Next.js
  • Micro-frontends
  • Build Tools and Bundling
  • Accessibility
  • Error Handling
  • Internationalization
  • Animation
  • Rapid Recall and Mock Frontend Rounds
  • Practice Test: Performance, Architecture and Production
Behavioral & Interview Day0/14
  • Resume and Project Walkthroughs
  • The STAR Method and Refining Your Stories
  • Conflict and Difficult Coworkers
  • Failures, Incidents, and Obstacles
  • Leadership, Mentorship, and Ownership
  • Teamwork, Communication, and Feedback
  • Growth and Handling Ambiguity
  • Career Goals, Why This Company, and Negotiation
  • Situational Questions
  • Company Research
  • Mock Behavioral Rounds
  • Interview Day
  • After the Interview
  • Practice Test: Behavioral and Interview Day
Browsing without an account — progress is saved in this browser only.Sign in to save it

Design an Expense-Sharing App (Splitwise)

Reading55 min

This is the money and algorithms problem of the section. The class model is genuinely small. What interviewers are actually checking is whether you represent money correctly, whether your split rules can be swapped out, and whether you can produce the settlement algorithm — "work out the fewest transactions needed to clear everyone's debts" — the moment they ask.

Step 1 — Requirements and the money rules

What the system needs to do:

  • Add users, and let them form groups.
  • Add an expense: who paid, how much, who shares it, and how the split is worked out.
  • Support equal, exact, and percentage splits — and be ready to add more later.
  • Show each user's balance: who owes whom, and how much.
  • Simplify debts: settle up a whole group in the fewest transactions possible.
  • Record a settlement payment between two people.

The money rules, stated up front — say these immediately, because most people skip them entirely:

  1. Never use floating-point numbers for money. Store whole numbers in the smallest unit — paise, not rupees. In binary floating point, 0.1 + 0.2 doesn't equal 0.3, and an expense report that's off by one paisa is a real bug report waiting to happen.
  2. A split has to add up exactly to the total. Splitting ₹100 three ways gives 33.33... rupees each. The standard fix is to hand the leftover remainder to the first few participants, or to the person who paid. Whichever you pick, say it out loud — an unassigned remainder is money that just vanishes.
  3. Balances are worked out on the fly. They're never stored as the actual source of truth. Keep the list of expenses, unchangeable, and calculate balances from that list whenever you need them. This is the same reasoning behind any accounting ledger, and it means every balance can always be explained, and every calculation can always be redone from scratch.

The clarifying questions:

Question Answer used here What it changes
What split types? Equal, exact, percentage — more coming later Strategy
Can more than one person pay for one expense? Yes paid_by is a map, not just a single person
Multiple currencies? Out of scope, but worth mentioning Would need Money to carry a currency and a conversion rule
Do we need a full history? Yes Expenses are unchangeable records, never edited after the fact
Group and non-group expenses? Both A group is just a convenience — not required for an expense to exist

Remember: store money as whole paise, never as a float. And every split must be checked to add up exactly to the total — never just assumed.

Sign in to try this knowledge check.

Step 2 — Entities and the split strategies

Concept Kind of thing Notes
User Entity id, name
Group Entity A named set of users
Money Value object Never changes, whole paise
Expense Entity, never changes once created description, total, paid_by, calculated shares
SplitStrategy Interface Equal / exact / percentage / share-units
BalanceSheet Service Works out net balances from the expense list
SettlementService Service The debt-simplification algorithm
User ◇──── Group
  ▲           ▲
  │           │
  └──── Expense ──> SplitStrategy (interface)
          - total: Money                △
          - paid_by: {user: paise}      │
          - shares:  {user: paise}   Equal / Exact / Percentage
                │
                ▼
          BalanceSheet  →  SettlementService.simplify()

Why Expense should never change once created: editing an expense after the fact would quietly change balances that people already acted on, without any warning. Real accounting systems fix mistakes with a new, offsetting entry — the same idea as a ledger correction. If someone asks for editing support, propose "add a corrected version" rather than mutating the original.

Why paid_by is a map, not just one person: two friends can easily split the bill at the restaurant together. Forcing the payer to be a single person means creating an awkward second expense just to represent that.

Remember: an expense once written down should never change. Corrections are new entries, not edits to the old one.

Sign in to try this knowledge check.

Step 3 — The debt simplification algorithm

Here's the question you'll get asked directly: "n people owe each other various amounts. What's the fewest transactions needed to settle everyone up?"

Step 1 — reduce it all down to net balances. All that actually matters, per person, is one single number: total paid, minus total owed. Positive means they should receive money; negative means they should pay. Just this step alone collapses a tangled web of individual debts (A owes B, B owes C, C owes A) into something you can actually settle cleanly.

Step 2 — greedily pair up the biggest creditor with the biggest debtor. Take whoever is owed the most and whoever owes the most, transfer the smaller of the two amounts between them, and repeat. Every single transaction brings at least one person's balance down to exactly zero, so with n people whose balance isn't zero, you'll need at most n − 1 transactions.

Balances: A +60, B −40, C −20
  match A(+60) with B(−40) → B pays A 40 → A +20, B 0
  match A(+20) with C(−20) → C pays A 20 → all zero
  2 transactions for 3 people — the n−1 bound holds

Here's the honest part, and it's the senior half of the answer: this greedy method is not guaranteed to be the true minimum. Finding the actual smallest number of transactions turns out to be an NP-hard problem — it's really about finding every possible group of people whose balances happen to add up to zero, and that search itself is the hard part. Greedy always stays at or under n − 1 transactions, and it's genuinely what production systems use. Naming this limitation earns you more credit than just presenting greedy as if it were provably optimal.

On complexity: sorting or using a heap gives you O(n log n) work per transaction, with at most n − 1 transactions total.

Remember: greedy settlement guarantees at most n − 1 transactions. It's not provably the minimum — say that out loud, don't present it as optimal.

Sign in to try this knowledge check.

Step 4 — The implementation

Python
from abc import ABC, abstractmethod
from dataclasses import dataclass
from collections import defaultdict
import heapq
import itertools


@dataclass(frozen=True)
class Money:
    paise: int
    def __str__(self) -> str: return f"Rs {self.paise / 100:.2f}"


class SplitStrategy(ABC):
    """Returns {user: paise}, guaranteed to add up to exactly `total_paise`."""
    @abstractmethod
    def split(self, total_paise: int, participants: list[str], **kwargs) -> dict[str, int]: ...


class EqualSplit(SplitStrategy):
    def split(self, total_paise, participants, **kwargs):
        n = len(participants)
        if n == 0:
            raise ValueError("no participants")
        base, remainder = divmod(total_paise, n)
        # A clear, repeatable rule: the first `remainder` people pay 1 extra paisa each.
        return {u: base + (1 if i < remainder else 0) for i, u in enumerate(participants)}


class ExactSplit(SplitStrategy):
    def split(self, total_paise, participants, *, amounts: dict[str, int], **kwargs):
        if sum(amounts.values()) != total_paise:
            raise ValueError(f"exact shares sum to {sum(amounts.values())}, not {total_paise}")
        if set(amounts) != set(participants):
            raise ValueError("amounts must cover exactly the participants")
        return dict(amounts)


class PercentageSplit(SplitStrategy):
    def split(self, total_paise, participants, *, percentages: dict[str, float], **kwargs):
        if abs(sum(percentages.values()) - 100.0) > 1e-9:
            raise ValueError("percentages must sum to 100")
        shares = {u: int(total_paise * percentages[u] / 100) for u in participants}
        # Rounding down loses a little; give the shortfall to the largest shares, deterministically.
        shortfall = total_paise - sum(shares.values())
        for u in sorted(shares, key=lambda x: (-shares[x], x))[:shortfall]:
            shares[u] += 1
        return shares


@dataclass(frozen=True)
class Expense:                                     # never changes once created
    expense_id: str
    description: str
    total_paise: int
    paid_by: tuple[tuple[str, int], ...]           # ((user, paise), ...) — supports co-payers
    shares: tuple[tuple[str, int], ...]            # ((user, paise), ...)


class ExpenseBook:
    def __init__(self):
        self._expenses: list[Expense] = []
        self._ids = itertools.count(1)

    def add(self, description: str, total_paise: int, paid_by: dict[str, int],
            participants: list[str], strategy: SplitStrategy, **kwargs) -> Expense:
        if sum(paid_by.values()) != total_paise:
            raise ValueError("payments must sum to the expense total")
        shares = strategy.split(total_paise, participants, **kwargs)
        if sum(shares.values()) != total_paise:    # always double-checked, never just assumed
            raise AssertionError("split does not sum to total")
        expense = Expense(f"E{next(self._ids)}", description, total_paise,
                          tuple(paid_by.items()), tuple(shares.items()))
        self._expenses.append(expense)
        return expense

    def settle(self, payer: str, payee: str, paise: int) -> Expense:
        """A settlement is really just an expense the payee owes entirely to the payer."""
        return self.add(f"settlement {payer}->{payee}", paise,
                        paid_by={payer: paise}, participants=[payee], strategy=ExactSplit(),
                        amounts={payee: paise})

    def balances(self) -> dict[str, int]:
        """Each user's net position: positive means owed money, negative means owing it."""
        net: dict[str, int] = defaultdict(int)
        for e in self._expenses:
            for user, paid in e.paid_by:
                net[user] += paid
            for user, share in e.shares:
                net[user] -= share
        return {u: v for u, v in net.items() if v != 0}

    def simplify(self) -> list[tuple[str, str, int]]:
        """Greedy: match the largest creditor with the largest debtor. At most n-1 transfers."""
        net = self.balances()
        # Max-heaps via negated keys; tie-break on name so the output stays predictable.
        creditors = [(-v, u) for u, v in net.items() if v > 0]
        debtors = [(v, u) for u, v in net.items() if v < 0]
        heapq.heapify(creditors)
        heapq.heapify(debtors)

        transfers: list[tuple[str, str, int]] = []
        while creditors and debtors:
            credit, creditor = heapq.heappop(creditors)
            debt, debtor = heapq.heappop(debtors)
            amount = min(-credit, -debt)
            transfers.append((debtor, creditor, amount))       # the debtor pays the creditor
            if -credit - amount > 0:
                heapq.heappush(creditors, (credit + amount, creditor))
            if -debt - amount > 0:
                heapq.heappush(debtors, (debt + amount, debtor))
        return transfers


book = ExpenseBook()

# Dinner: Asha pays Rs 1000, split equally three ways -> 33333/33333/33334 paise
dinner = book.add("dinner", 100_000, {"asha": 100_000},
                  ["asha", "ravi", "meera"], EqualSplit())
assert sum(dict(dinner.shares).values()) == 100_000
assert sorted(dict(dinner.shares).values()) == [33_333, 33_333, 33_334]

# Cab: Ravi and Meera co-pay; exact shares
book.add("cab", 60_000, {"ravi": 40_000, "meera": 20_000},
         ["asha", "ravi", "meera"], ExactSplit(),
         amounts={"asha": 30_000, "ravi": 20_000, "meera": 10_000})

# Groceries: a percentage split
book.add("groceries", 45_000, {"meera": 45_000},
         ["asha", "ravi", "meera"], PercentageSplit(),
         percentages={"asha": 50, "ravi": 25, "meera": 25})

net = book.balances()
assert sum(net.values()) == 0                     # the money always balances out to zero

transfers = book.simplify()
assert len(transfers) <= len(net) - 1             # the n-1 bound
assert sum(amount for _, _, amount in transfers) == sum(v for v in net.values() if v > 0)

for debtor, creditor, amount in transfers:
    book.settle(debtor, creditor, amount)
assert book.balances() == {}                      # everything settles down to zero

print("net before settling:", {u: str(Money(v)) for u, v in net.items()})
print("transfers:", [(d, c, str(Money(a))) for d, c, a in transfers])
Sign in to try this knowledge check.

Steps 5 and 6 — Concurrency and extensions

The shared, changeable data: the expense list. Because expenses are only ever added, never changed, concurrency here turns out to be unusually easy:

Concern How it's handled
Two people add expenses at the same moment Additions are independent of each other; balances are always recalculated from the list. No lock needed beyond the append itself
Two people settle the same debt at the same moment Both settlements get recorded; the balance goes negative, which shows up as an overpayment. Fix it with an idempotency key per settlement, or a conditional insert keyed on (group, payer, payee, amount, client_ref)
A slightly stale balance shown on screen Perfectly acceptable — and worth saying so. This is eventual consistency, with a clear reason behind it
Caching balances for speed Treat the cache as something you can always recalculate — recompute it or throw it away on every new expense, and never treat it as the real source of truth

The general lesson worth stating: unchangeable, append-only data makes concurrency almost free. That's exactly why every real ledger is designed this way.

Extensions, and how they fit in:

Requirement What changes
A new split type (by shares, like "2 shares to Asha, 1 to Ravi") A new SplitStrategy class; nothing else moves
Multiple currencies Money gains a currency field; a ConversionPolicy locks in the exchange rate at the moment the expense is created, and stores it — never recalculate old balances using today's rate
Recurring expenses (rent, subscriptions) A scheduler that creates new expenses on a timer; the model itself is unchanged
Attaching receipts to an expense A field on Expense; the actual files live in object storage, not the database
Simplifying debts per group vs. across everyone The settlement service just takes a filtered balance map — same algorithm, different input
Notifying people when someone adds an expense An Observer on expense.added
Editing an expense Never edit it directly — mark the original as reversed with an offsetting entry, then add the corrected version. History stays intact either way

Remember: if multi-currency ever comes up, the exchange rate gets locked in at the moment of the expense, never recalculated later at today's rate.

Sign in to try this knowledge check.

Quick recap

  • State the money rules first: whole paise, an explicit rule for leftover remainders, and shares that are checked to add up exactly to the total — never just assumed to.
  • Expenses never change; balances are calculated. This gets you explainability, the ability to redo any calculation from scratch, and concurrency that's nearly free — the same ledger reasoning that shows up in real payment systems.
  • The split type is the strategy, and it's exactly the axis interviewers will ask you to extend.
  • The settlement algorithm is net balances, then greedily matching the biggest creditor with the biggest debtor, bounded at n − 1 transfers — and you should volunteer that greedy isn't provably optimal, since finding the true minimum is NP-hard.
  • paid_by as a map handles people co-paying without twisting the model out of shape, and it costs nothing extra to support from the start.
PreviousDesign a Movie Ticket Booking SystemNextDesign a Vending Machine and an ATM

Course progress

0%
0 of 177 modules complete

On this page

  • Step 1 — Requirements and the money rules
  • Step 2 — Entities and the split strategies
  • Step 3 — The debt simplification algorithm
  • Step 4 — The implementation
  • Steps 5 and 6 — Concurrency and extensions
  • Quick recap