Skip to main content

Design a Splitwise system

Problem Statement

Design a Splitwise-like system that allows users to split expenses among friends. The system should support equal splitting, exact amounts, and percentage-based splitting. It should also keep track of balances between users and help users understand who owes whom.


Functional Requirements

  • Users should be able to create expenses
  • Expenses can be split equally, by exact amount, or by percentage
  • The system should maintain balances between users
  • Users should be able to see how much they owe or are owed
  • The system should support multiple groups
  • Each group contains multiple users and expenses

Objects Required

  • User
  • Group
  • Expense
  • Split
  • EqualSplit
  • ExactSplit
  • PercentSplit
  • ExpenseService
  • BalanceSheet

User Class


public class User {

    private String userId;
    private String name;

    public User(String userId, String name) {
        this.userId = userId;
        this.name = name;
    }

    public String getUserId() {
        return userId;
    }
}

The User class represents a person using the system. It stores basic identity details like userId and name.

The constructor ensures every user is created with valid identity data so we don’t end up with partially defined users in the system.

The getUserId() method is used everywhere we need to uniquely identify a user without directly exposing internal fields.


Split Abstract Class


public abstract class Split {

    User user;
    double amount;

    public Split(User user) {
        this.user = user;
    }

    public abstract double getAmount();
}

The Split class represents how an expense is divided among users. It is kept abstract because different splitting strategies behave differently.

The constructor assigns the user who is part of the split. The actual amount logic is left to child classes because equal, exact, and percentage splits all behave differently.

The getAmount() method forces each subclass to define how much that user owes.


EqualSplit Class


public class EqualSplit extends Split {

    private double totalAmount;
    private int totalUsers;

    public EqualSplit(User user, double totalAmount, int totalUsers) {
        super(user);
        this.totalAmount = totalAmount;
        this.totalUsers = totalUsers;
    }

    @Override
    public double getAmount() {
        return totalAmount / totalUsers;
    }
}

This class handles equal splitting. The idea is simple: everyone pays the same share.

The constructor takes total amount and number of users involved in the expense.

The getAmount() method divides the total equally. This keeps the logic clean and avoids recalculating it elsewhere.


ExactSplit Class


public class ExactSplit extends Split {

    private double exactAmount;

    public ExactSplit(User user, double exactAmount) {
        super(user);
        this.exactAmount = exactAmount;
    }

    @Override
    public double getAmount() {
        return exactAmount;
    }
}

ExactSplit is used when we already know exactly how much a user should pay.

The constructor stores the exact amount assigned to a user.

The getAmount() method simply returns that value without any calculation.


PercentSplit Class


public class PercentSplit extends Split {

    private double percent;
    private double totalAmount;

    public PercentSplit(User user, double percent, double totalAmount) {
        super(user);
        this.percent = percent;
        this.totalAmount = totalAmount;
    }

    @Override
    public double getAmount() {
        return (percent / 100.0) * totalAmount;
    }
}

PercentSplit calculates the amount based on percentage contribution.

The constructor stores both percentage and total expense value.

The getAmount() method converts percentage into actual money owed.


Expense Class


import java.util.*;

public class Expense {

    private String expenseId;
    private double totalAmount;
    private User paidBy;
    private List splits;

    public Expense(String expenseId,
                   double totalAmount,
                   User paidBy,
                   List splits) {

        this.expenseId = expenseId;
        this.totalAmount = totalAmount;
        this.paidBy = paidBy;
        this.splits = splits;
    }

    public List getSplits() {
        return splits;
    }

    public User getPaidBy() {
        return paidBy;
    }

    public double getTotalAmount() {
        return totalAmount;
    }
}

The Expense class represents a single shared cost.

The constructor binds together who paid, how much was paid, and how it should be split.

getSplits() is used when updating balances between users.

getPaidBy() helps identify who should be credited for the payment.

getTotalAmount() returns the full expense value used for validation and balance updates.


BalanceSheet Class


import java.util.*;

public class BalanceSheet {

    private Map balances = new HashMap<>();

    public void updateBalance(User paidBy, User owedBy, double amount) {

        String key = paidBy.getUserId() + "->" + owedBy.getUserId();

        balances.put(key,
                balances.getOrDefault(key, 0.0) + amount);
    }

    public Map getBalances() {
        return balances;
    }
}

BalanceSheet keeps track of who owes money to whom.

updateBalance() increases or decreases debt between two users whenever an expense is added.

It uses a simple key format (payer → borrower) to store relationships in memory.

getBalances() helps fetch all outstanding balances in the system.


ExpenseService Class


import java.util.*;

public class ExpenseService {

    private BalanceSheet balanceSheet = new BalanceSheet();

    public void addExpense(Expense expense) {

        User paidBy = expense.getPaidBy();
        double totalAmount = expense.getTotalAmount();

        for (Split split : expense.getSplits()) {

            double amount = split.getAmount();

            if (!split.user.getUserId()
                    .equals(paidBy.getUserId())) {

                balanceSheet.updateBalance(
                        paidBy,
                        split.user,
                        amount
                );
            }
        }
    }

    public BalanceSheet getBalanceSheet() {
        return balanceSheet;
    }
}

This is the core service of the system where expenses are actually processed.

addExpense() takes a new expense and breaks it down into individual splits. Then it updates balances between users one by one.

It avoids updating balance for the person who paid the expense.

getBalanceSheet() simply exposes current balances for reporting.


Main Class


import java.util.*;

public class Main {

    public static void main(String[] args) {

        User u1 = new User("U1", "A");
        User u2 = new User("U2", "B");
        User u3 = new User("U3", "C");

        Split s1 = new EqualSplit(u1, 300, 3);
        Split s2 = new EqualSplit(u2, 300, 3);
        Split s3 = new EqualSplit(u3, 300, 3);

        Expense expense = new Expense(
                "E1",
                300,
                u1,
                Arrays.asList(s1, s2, s3)
        );

        ExpenseService service = new ExpenseService();
        service.addExpense(expense);

        System.out.println(service.getBalanceSheet().getBalances());
    }
}

The main method simulates a real-world splitwise flow.

We create users, define how the expense should be split, and then pass it to the service.

Finally, the balance sheet is printed to show how money is distributed among users.


Class Diagram

UseruserId : Stringname : StringSplituser : UsergetAmount() : doubleEqualSplittotalAmount : doubletotalUsers : intgetAmount() : doubleExactSplitamount : doublegetAmount() : doublePercentSplitpercent : doubletotalAmount : doublegetAmount() : doubleExpenseexpenseId : StringtotalAmount : doublepaidBy : Usersplits : List<Split>BalanceSheetbalances : Map<String, Double>updateBalance(paidBy : User, owedBy : User, amount : double) : voidExpenseServiceaddExpense(expense : Expense) : voidgetBalanceSheet() : BalanceSheetMainmain(args : String[]) : void

Also See

Comments

Popular posts from this blog

Designing a Parking Lot - Low Level Design

Problem Statement Design a parking lot that can handle vehicles entering and leaving while managing parking across multiple floors. Each vehicle should be assigned a suitable parking spot based on its type, and the spot should be freed once the vehicle exits. The design should also support generating a ticket at entry and optionally calculating the parking fee based on the duration of stay. Asked In Companies Amazon Google Microsoft Uber Walmart Flipkart Meta PayPal Oracle Salesforce Adobe Apple Intuit LinkedIn Atlassian Functional Requirements The design should support multiple vehicle types such as bikes, cars, and trucks A vehicle must be assigned a parking spot compatible with its type A parking spot cannot be assigned to more than one vehicle at a time The parking lot should support multiple levels (floors) The design should search and allocate an availa...

Most Frequently Asked Low Level Design(LLD) Interview Questions

Below are the curated list of most commonly asked Low Level Design (LLD) interview problems. Each problem includes a short description and a link to the complete solution with code and class diagrams. Design Parking Lot System The system should handle parking for different vehicle types such as bikes, cars, and trucks. It should manage slot allocation, availability tracking, and entry/exit flow. The design also ensures efficient usage of parking space under varying load conditions. View Solution Design Elevator / Lift System The system should support multiple elevators operating across floors with request handling logic. It focuses on scheduling algorithms to minimize wait time and optimize movement. It also manages direction control and concurrent floor requests. View Solution Design Movie Ticket Booking System The system should allow users to browse movies, select shows, and book seats. It handles seat ...

Software Design Patterns for LLD Interviews: A Complete Guide

Software Design Patterns for LLD Interviews: A Complete Guide In Software Development Engineer (SDE) interviews—especially for mid-level and senior roles—low-level design (LLD) rounds assess your ability to write clean, reusable, maintainable, and extensible code. The foundation of resolving these architectural challenges lies in the standard Gang of Four (GoF) Design Patterns. Rather than memorizing theoretical definitions, interviewers expect you to apply these patterns to real-world scenarios, identifying the trade-offs of each. Below is a comprehensive guide to the 12 most frequently asked design patterns in LLD interviews, categorized by their classification (Creational, Structural, and Behavioral). Each pattern contains a concrete, real-world Java implementation and a detailed breakdown of design decisions. Creational Design Patterns Creational design patterns deal with object creation mechanisms. They abstract the instantiation process, making a system independent of how...