đź“… Week-7 (Day-1) - How to Build a Splitwise Clone: System Design for an Expense Splitting App

Payal KumariPayal Kumari
16 min read

NOTE: - I started my 8-week system design journey with Coder Army. I will be journaling every day, recording what I learn, reflecting on it, and sharing it with my network to help newcomers to system design.

Namaste developers! 👋 Welcome to another day of the #8WeeksLLDChallenge where we dive into a real-world app that we’ve all used (and needed!) — Splitwise. From abroad trips to lunch bills, Splitwise helps manage who owes what — cleanly and fairly.

Let’s break down how to build a Splitwise-like system using design patterns like Strategy, Observer, Singleton, and Factory — all while keeping things simple, scalable, and reusable.

(Hindi:- Ever wondered kaise apps like Splitwise manage karte hain complex expense sharing between friends, roommates, ya trip members? Aaj hum banayenge ek aisa system jo not only handles expense splits but also smartly simplifies debts!)

đź’ Real-World Use Case

Imagine a situation:

  • Rohit paid for the hotel (Rs. 200)

  • Saurav paid for dinner (Rs. 200)

  • Manish paid for transport (Rs. 200)

Everyone was supposed to contribute equally, right?

But now there's confusion about who owes how much to whom?

👉 A Splitwise Clone will handle all these calculations and settlements. It simplifies debt and tracks balances automatically.

(Hindi:- Imagine ek situation:

  • Rohit ne hotel ke liye pay kiya (Rs. 200)

  • Saurav ne dinner ke liye (Rs. 200)

  • Manish ne transport ke liye (Rs. 200)

  • Sabko equally contribute karna chahiye tha, right?

Par ab confusion hai ki kaun kisko kitna de?

👉 Splitwise Clone ye sari calculations aur settlements ko handle karega. Aur debt simplification logic use karke sabse minimum transactions ke saath settle karega! )

đź’ System Design Breakdown (LLD)

Technologies & Concepts Used:

  • Java / C++ (No third-party libs)

  • OOP + SOLID Principles

  • Design Patterns: Strategy, Factory, Observer, Singleton

đź’ Core Features (Requirements):

  • User can join / leave group

  • Add expenses in a group

  • Settle expenses

  • Different split strategies: Equal, Exact, Percentage

  • User can't leave until they settle dues

  • Add one-on-one individual expenses

  • Notifications on expense/settlement

đź’ Design Patterns with Real Life Examples

🎯 Strategy Pattern

To support different types of split methods:

  • EqualSplit: Split the amount equally among all users

  • ExactSplit: Specify the exact amount each user needs to pay

  • PercentageSplit: Distribute the amount based on a percentage

The Strategy Pattern is used to handle these different splitting methods. Each strategy is implemented as a separate class.

(Hindi:- Different split methods ke liye:

  • EqualSplit: Sabko barabar split karo

  • ExactSplit: Har ek ka exact amount specify karo

  • PercentageSplit: % wise distribution

Strategy Pattern alag-alag tarah ke splitting ko handle karta hai. Har strategy ko ek class banaya gaya hai.)

🎯 Factory Pattern

You don't need to write the logic again and again. A factory class will decide which strategy object to create.

The Factory Pattern centralizes the control of object creation.

(Hindi:- Aapko baar-baar logic likhne ki zarurat nahi hai. Ek factory class decide karegi kaunsa strategy object banana hai. Factory Pattern se object creation ka control centralized hota hai.)

🎯Observer Pattern

Whenever a new expense is added or a settlement is made — all group members will receive a notification!

(The Observer Pattern sends notifications whenever any change occurs.)

(Hindi:- Whenever koi naya expense add ho ya settlement ho — saare group members ko notification milega! Observer pattern notify karta hai jab bhi koi change hota hai.)

🎯Singleton Pattern

There should be only one instance of the Splitwise manager throughout the app — that’s why the Singleton Pattern is used.
( A centralized expense manager that handles users, groups, and transactions.)

(Hindi:- Pure app mein ek hi Splitwise manager instance ho — isliye Singleton pattern ka use kiya gaya hai.

Ek centralized expense manager jahan se users, groups aur transactions handle hote hain.)

import java.util.*;
import java.text.DecimalFormat;

// Forward declarations equivalent - not needed in Java due to automatic resolution

enum SplitType {
    EQUAL,
    EXACT,
    PERCENTAGE
}

class Split {
    public String userId;
    public double amount;

    public Split(String userId, double amount) {
        this.userId = userId;
        this.amount = amount;
    }
}

// Observer Pattern - Notification interface
interface Observer {
    void update(String message);
}

// Strategy Pattern - Split strategies
interface SplitStrategy {
    List<Split> calculateSplit(double totalAmount, List<String> userIds, List<Double> values);
}

class EqualSplit implements SplitStrategy {
    @Override
    public List<Split> calculateSplit(double totalAmount, List<String> userIds, List<Double> values) {
        List<Split> splits = new ArrayList<>();
        double amountPerUser = totalAmount / userIds.size();

        for (String userId : userIds) {
            splits.add(new Split(userId, amountPerUser));
        }
        return splits;
    }
}

class ExactSplit implements SplitStrategy {
    @Override
    public List<Split> calculateSplit(double totalAmount, List<String> userIds, List<Double> values) {
        List<Split> splits = new ArrayList<>();

        //validations

        for (int i = 0; i < userIds.size(); i++) {
            splits.add(new Split(userIds.get(i), values.get(i)));
        }
        return splits;
    }
}

class PercentageSplit implements SplitStrategy {
    @Override
    public List<Split> calculateSplit(double totalAmount, List<String> userIds, List<Double> values) {
        List<Split> splits = new ArrayList<>();

        //validations

        for (int i = 0; i < userIds.size(); i++) {
            double amount = (totalAmount * values.get(i)) / 100.0;
            splits.add(new Split(userIds.get(i), amount));
        }
        return splits;
    }
}

// Factory for split strategies
class SplitFactory {
    public static SplitStrategy getSplitStrategy(SplitType type) {
        switch (type) {
            case EQUAL:
                return new EqualSplit();
            case EXACT:
                return new ExactSplit();
            case PERCENTAGE:
                return new PercentageSplit();
            default:
                return new EqualSplit();
        }
    }
}

// User class --> Concrete Observer
class User implements Observer {
    public static int nextUserId = 0;
    public String userId;
    public String name;
    public String email;
    public Map<String, Double> balances; // userId -> amount (positive = they owe you, negative = you owe them)

    public User(String name, String email) {
        this.userId = "user" + (++nextUserId);
        this.name = name;
        this.email = email;
        this.balances = new HashMap<>();
    }

    @Override
    public void update(String message) {
        System.out.println("[NOTIFICATION to " + name + "]: " + message);
    }

    public void updateBalance(String otherUserId, double amount) {
        balances.put(otherUserId, balances.getOrDefault(otherUserId, 0.0) + amount);

        // Remove if balance becomes zero
        if (Math.abs(balances.get(otherUserId)) < 0.01) {
            balances.remove(otherUserId);
        }
    }

    public double getTotalOwed() {
        double total = 0;
        for (Map.Entry<String, Double> balance : balances.entrySet()) {
            if (balance.getValue() < 0) {
                total += Math.abs(balance.getValue());
            }
        }
        return total;
    }

    public double getTotalOwing() {
        double total = 0;
        for (Map.Entry<String, Double> balance : balances.entrySet()) {
            if (balance.getValue() > 0) {
                total += balance.getValue();
            }
        }
        return total;
    }
}

// Expense Model class
class Expense {
    public static int nextExpenseId = 0;
    public String expenseId;
    public String description;
    public double totalAmount;
    public String paidByUserId;
    public List<Split> splits;
    public String groupId;

    public Expense(String desc, double amount, String paidBy,
            List<Split> splits, String group) {
        this.expenseId = "expense" + (++nextExpenseId);
        this.description = desc;
        this.totalAmount = amount;
        this.paidByUserId = paidBy;
        this.splits = splits;
        this.groupId = group;
    }

    public Expense(String desc, double amount, String paidBy, List<Split> splits) {
        this(desc, amount, paidBy, splits, "");
    }
}

class DebtSimplifier {
    public static Map<String, Map<String, Double>> simplifyDebts(
        Map<String, Map<String, Double>> groupBalances) {

        // Calculate net amount for each person
        Map<String, Double> netAmounts = new HashMap<>();

        // Initialize all users with 0
        for (Map.Entry<String, Map<String, Double>> userBalance : groupBalances.entrySet()) {
            netAmounts.put(userBalance.getKey(), 0.0);
        }

        // Calculate net amounts
        // We only need to process each balance once (not twice)
        // If groupBalances[A][B] = 200, it means B owes A 200
        // So A should receive 200 (positive) and B should pay 200 (negative)
        for (Map.Entry<String, Map<String, Double>> userBalance : groupBalances.entrySet()) {
            String creditorId = userBalance.getKey();
            for (Map.Entry<String, Double> balance : userBalance.getValue().entrySet()) {
                String debtorId = balance.getKey();
                double amount = balance.getValue();

                // Only process positive amounts to avoid double counting
                if (amount > 0) {
                    netAmounts.put(creditorId, netAmounts.get(creditorId) + amount);  // creditor receives
                    netAmounts.put(debtorId, netAmounts.get(debtorId) - amount);    // debtor pays
                }
            }
        }

        // Divide users into creditors and debtors
        List<AbstractMap.SimpleEntry<String, Double>> creditors = new ArrayList<>(); // those who should receive money
        List<AbstractMap.SimpleEntry<String, Double>> debtors = new ArrayList<>();   // those who should pay money

        for (Map.Entry<String, Double> net : netAmounts.entrySet()) {
            if (net.getValue() > 0.01) { // creditor
                creditors.add(new AbstractMap.SimpleEntry<>(net.getKey(), net.getValue()));
            } else if (net.getValue() < -0.01) { // debtor
                debtors.add(new AbstractMap.SimpleEntry<>(net.getKey(), -net.getValue())); // store positive amount
            }
        }

        // Sort for better optimization (largest amounts first)
        creditors.sort((a, b) -> Double.compare(b.getValue(), a.getValue()));
        debtors.sort((a, b) -> Double.compare(b.getValue(), a.getValue()));

        // Create new simplified balance map
        Map<String, Map<String, Double>> simplifiedBalances = new HashMap<>();

        // Initialize empty maps for all users
        for (Map.Entry<String, Map<String, Double>> userBalance : groupBalances.entrySet()) {
            simplifiedBalances.put(userBalance.getKey(), new HashMap<>());
        }

        // Use greedy algorithm to minimize transactions
        int i = 0, j = 0;
        while (i < creditors.size() && j < debtors.size()) {
            String creditorId = creditors.get(i).getKey();
            String debtorId = debtors.get(j).getKey();
            double creditorAmount = creditors.get(i).getValue();
            double debtorAmount = debtors.get(j).getValue();

            // Find the minimum amount to settle
            double settleAmount = Math.min(creditorAmount, debtorAmount);

            // Update simplified balances
            // debtorId owes creditorId the settleAmount
            simplifiedBalances.get(creditorId).put(debtorId, settleAmount);
            simplifiedBalances.get(debtorId).put(creditorId, -settleAmount);

            // Update remaining amounts
            creditors.get(i).setValue(creditors.get(i).getValue() - settleAmount);
            debtors.get(j).setValue(debtors.get(j).getValue() - settleAmount);

            // Move to next creditor or debtor if current one is settled
            if (creditors.get(i).getValue() < 0.01) {
                i++;
            }
            if (debtors.get(j).getValue() < 0.01) {
                j++;
            }
        }

        return simplifiedBalances;
    }
}

// Group class --> Concrete Observable
class Group {
    private User getUserByuserId(String userId) {
        User user = null;

        for(User member : members) {
            if(member.userId.equals(userId)) {
                user = member;
            }
        }
        return user;
    }

    public static int nextGroupId = 0;
    public String groupId;
    public String name;
    public List<User> members; //observers
    public Map<String, Expense> groupExpenses; // Group's own expense book
    public Map<String, Map<String, Double>> groupBalances; // memberId -> {otherMemberId -> balance}

    public Group(String name) {
        this.groupId = "group" + (++nextGroupId);
        this.name = name;
        this.members = new ArrayList<>();
        this.groupExpenses = new HashMap<>();
        this.groupBalances = new HashMap<>();
    }

    public void addMember(User user) {
        members.add(user);

        // Initialize balance map for new member
        groupBalances.put(user.userId, new HashMap<>());
        System.out.println(user.name + " added to group " + name);
    }

    public boolean removeMember(String userId) {    
        // Check if user can be removed or not
        if(!canUserLeaveGroup(userId)) {
            System.out.println("\nUser not allowed to leave group without clearing expenses");
            return false;
        }

        // Remove from observers
        members.removeIf(user -> user.userId.equals(userId));

        // Remove from group balances
        groupBalances.remove(userId);

        // Remove this user from other members' balance maps
        for (Map.Entry<String, Map<String, Double>> memberBalance : groupBalances.entrySet()) {
            memberBalance.getValue().remove(userId);
        }
        return true;
    }

    public void notifyMembers(String message) {
        for (Observer observer : members) {
            observer.update(message);
        }
    }

    public boolean isMember(String userId) {
        return groupBalances.containsKey(userId);
    }

    // Update balance within group
    public void updateGroupBalance(String fromUserId, String toUserId, double amount) {
        groupBalances.get(fromUserId).put(toUserId, 
            groupBalances.get(fromUserId).getOrDefault(toUserId, 0.0) + amount);
        groupBalances.get(toUserId).put(fromUserId, 
            groupBalances.get(toUserId).getOrDefault(fromUserId, 0.0) - amount);

        // Remove if balance becomes zero
        if (Math.abs(groupBalances.get(fromUserId).get(toUserId)) < 0.01) {
            groupBalances.get(fromUserId).remove(toUserId);
        }
        if (Math.abs(groupBalances.get(toUserId).get(fromUserId)) < 0.01) {
            groupBalances.get(toUserId).remove(fromUserId);
        }
    }

    // Check if user can leave group.
    public boolean canUserLeaveGroup(String userId) {
        if (!isMember(userId)) {
            throw new RuntimeException("user is not a part of this group");
        }

        // Check if user has any outstanding balance with other group members
        Map<String, Double> userBalanceSheet = groupBalances.get(userId);
        for (Map.Entry<String, Double> balance : userBalanceSheet.entrySet()) {
            if (Math.abs(balance.getValue()) > 0.01) {
                return false; // Has outstanding balance
            }
        }
        return true;
    }

    // Get user's balance within this group
    public Map<String, Double> getUserGroupBalances(String userId) {
        if (!isMember(userId)) {
            throw new RuntimeException("user is not a part of this group");
        }
        return groupBalances.get(userId);
    }

    // Add expense to this group
    public boolean addExpense(String description, double amount, String paidByUserId,
                   List<String> involvedUsers, SplitType splitType, 
                   List<Double> splitValues) {

        if (!isMember(paidByUserId)) {
            throw new RuntimeException("user is not a part of this group");
        }

        // Validate that all involved users are group members
        for (String userId : involvedUsers) {
            if (!isMember(userId)) {
                throw new RuntimeException("involvedUsers are not a part of this group");
            }
        }

        // Generate splits using strategy pattern
        List<Split> splits = SplitFactory.getSplitStrategy(splitType)
                                .calculateSplit(amount, involvedUsers, splitValues);

        // Create expense in group's own expense book
        Expense expense = new Expense(description, amount, paidByUserId, splits, groupId);
        groupExpenses.put(expense.expenseId, expense);

        // Update group balances
        for (Split split : splits) {
            if (!split.userId.equals(paidByUserId)) {
                // Person who paid gets positive balance, person who owes gets negative
                updateGroupBalance(paidByUserId, split.userId, split.amount);
            }
        }

        System.out.println("\n=========== Sending Notifications ====================");
        String paidByName = getUserByuserId(paidByUserId).name;
        notifyMembers("New expense added: " + description + " (Rs " + amount + ")");

        // Printing console message-------
        System.out.println("\n=========== Expense Message ====================");
        System.out.println("Expense added to " + name + ": " + description + " (Rs " + amount 
             + ") paid by " + paidByName +" and involved people are : ");
        if(!splitValues.isEmpty()) {
            for(int i=0; i<splitValues.size(); i++) {
                System.out.println(getUserByuserId(involvedUsers.get(i)).name + " : " + splitValues.get(i));
            }
        } 
        else {
            for(String user : involvedUsers) {
                System.out.print(getUserByuserId(user).name + ", ");
            }
            System.out.println("\nWill be Paid Equally");
        }    
        //-----------------------------------

        return true;
    }

    public boolean addExpense(String description, double amount, String paidByUserId,
                   List<String> involvedUsers, SplitType splitType) {
        return addExpense(description, amount, paidByUserId, involvedUsers, splitType, new ArrayList<>());
    }

    public boolean settlePayment(String fromUserId, String toUserId, double amount) {
        // Validate that both users are group members
        if (!isMember(fromUserId) || !isMember(toUserId)) {
            System.out.println("user is not a part of this group");
            return false;
        }

        // Update group balances
        updateGroupBalance(fromUserId, toUserId, amount);

        // Get user names for display
        String fromName = getUserByuserId(fromUserId).name;
        String toName = getUserByuserId(toUserId).name;

        // Notify group members
        notifyMembers("Settlement: " + fromName + " paid " + toName + " Rs " + amount);

        System.out.println("Settlement in " + name + ": " + fromName + " settled Rs " 
             + amount + " with " + toName);

        return true;
    }

    public void showGroupBalances() {
        System.out.println("\n=== Group Balances for " + name + " ===");
        DecimalFormat df = new DecimalFormat("#.##");

        for (Map.Entry<String, Map<String, Double>> pair : groupBalances.entrySet()) {
            String memberId = pair.getKey();
            String memberName = getUserByuserId(memberId).name;

            System.out.println(memberName + "'s balances in group:");

            Map<String, Double> userBalances = pair.getValue();
            if (userBalances.isEmpty()) {
                System.out.println("  No outstanding balances");
            } 
            else {
                for (Map.Entry<String, Double> userBalance : userBalances.entrySet()) {
                    String otherMemberUserId = userBalance.getKey();
                    String otherName = getUserByuserId(otherMemberUserId).name;

                    double balance = userBalance.getValue();
                    if (balance > 0) {
                        System.out.println("  " + otherName + " owes: Rs " + df.format(balance));
                    } else {
                        System.out.println("  Owes " + otherName + ": Rs " + df.format(Math.abs(balance)));
                    }
                }
            }
        }
    }

    public void simplifyGroupDebts() {
        Map<String, Map<String, Double>> simplifiedBalances = DebtSimplifier.simplifyDebts(groupBalances);
        groupBalances = simplifiedBalances;

        System.out.println("\nDebts have been simplified for group: " + name);
    }
}

// Main ExpenseManager class (Singleton - Facade)
class Splitwise {
    private Map<String, User> users;
    private Map<String, Group> groups;
    private Map<String, Expense> expenses;

    private static Splitwise instance;

    private Splitwise() {
        users = new HashMap<>();
        groups = new HashMap<>();
        expenses = new HashMap<>();
    }

    public static Splitwise getInstance() {
        if(instance == null) {
            instance = new Splitwise();
        }
        return instance;
    }

    // User management
    public User createUser(String name, String email) {
        User user = new User(name, email);
        users.put(user.userId, user);
        System.out.println("User created: " + name + " (ID: " + user.userId + ")");
        return user;
    }

    public User getUser(String userId) {
        return users.get(userId);
    }

    // Group management
    public Group createGroup(String name) {
        Group group = new Group(name);
        groups.put(group.groupId, group);
        System.out.println("Group created: " + name + " (ID: " + group.groupId + ")");
        return group;
    }

    public Group getGroup(String groupId) {
        return groups.get(groupId);
    }

    public void addUserToGroup(String userId, String groupId) {
        User user = getUser(userId);
        Group group = getGroup(groupId);

        if (user != null && group != null) {
            group.addMember(user);
        }
    }

    // Try to remove user from group - just delegates to group
    public boolean removeUserFromGroup(String userId, String groupId) {
        Group group = getGroup(groupId);

        if (group == null) {
            System.out.println("Group not found!");
            return false;
        }

        User user = getUser(userId);
        if (user == null) {
            System.out.println("User not found!");
            return false;
        }

        boolean userRemoved = group.removeMember(userId);

        if(userRemoved) {
            System.out.println(user.name + " successfully left " + group.name);
        }
        return userRemoved;
    }

    // Expense management - delegate to group
    public void addExpenseToGroup(String groupId, String description, double amount, 
                          String paidByUserId, List<String> involvedUsers, 
                          SplitType splitType, List<Double> splitValues) {

        Group group = getGroup(groupId);
        if (group == null) {
            System.out.println("Group not found!");
            return;
        }

        group.addExpense(description, amount, paidByUserId, involvedUsers, splitType, splitValues);
    }

    public void addExpenseToGroup(String groupId, String description, double amount, 
                          String paidByUserId, List<String> involvedUsers, 
                          SplitType splitType) {
        addExpenseToGroup(groupId, description, amount, paidByUserId, involvedUsers, splitType, new ArrayList<>());
    }

    // Settlement - delegate to group
    public void settlePaymentInGroup(String groupId, String fromUserId, 
                              String toUserId, double amount) {

        Group group = getGroup(groupId);
        if (group == null) {
            System.out.println("Group not found!");
            return;
        }

        group.settlePayment(fromUserId, toUserId, amount);
    }

    // Settlement
    public void settleIndividualPayment(String fromUserId, String toUserId, double amount) {
        User fromUser = getUser(fromUserId);
        User toUser = getUser(toUserId);

        if (fromUser != null && toUser != null) {
            fromUser.updateBalance(toUserId, amount);
            toUser.updateBalance(fromUserId, -amount);

            System.out.println(fromUser.name + " settled Rs" + amount + " with " + toUser.name);
        }
    }

    public void addIndividualExpense(String description, double amount, String paidByUserId,
                             String toUserId, SplitType splitType,
                            List<Double> splitValues) {

        SplitStrategy strategy = SplitFactory.getSplitStrategy(splitType);
        List<Split> splits = strategy.calculateSplit(amount, Arrays.asList(paidByUserId, toUserId), splitValues);

        Expense expense = new Expense(description, amount, paidByUserId, splits);
        expenses.put(expense.expenseId, expense);

        User paidByUser = getUser(paidByUserId);
        User toUser = getUser(toUserId);

        paidByUser.updateBalance(toUserId, amount);
        toUser.updateBalance(paidByUserId, -amount);

        System.out.println("Individual expense added: " + description + " (Rs " + amount 
                + ") paid by " + paidByUser.name +" for " + toUser.name);
    }

    public void addIndividualExpense(String description, double amount, String paidByUserId,
                             String toUserId, SplitType splitType) {
        addIndividualExpense(description, amount, paidByUserId, toUserId, splitType, new ArrayList<>());
    }

    // Display Method
    public void showUserBalance(String userId) {
        User user = getUser(userId);
        if (user == null) return;

        DecimalFormat df = new DecimalFormat("#.##");
        System.out.println("\n=========== Balance for " + user.name +" ===================="); 
        System.out.println("Total you owe: Rs " + df.format(user.getTotalOwed()));
        System.out.println("Total others owe you: Rs " + df.format(user.getTotalOwing()));

        System.out.println("Detailed balances:");
        for (Map.Entry<String, Double> balance : user.balances.entrySet()) {
            User otherUser = getUser(balance.getKey());
            if (otherUser != null) {
                if (balance.getValue() > 0) {
                    System.out.println("  " + otherUser.name + " owes you: Rs" + balance.getValue());
                } else {
                    System.out.println("  You owe " + otherUser.name + ": Rs" + Math.abs(balance.getValue()));
                }
            }
        }
    }

    public void showGroupBalances(String groupId) {
        Group group = getGroup(groupId);
        if (group == null) return;

        group.showGroupBalances();
    }

    public void simplifyGroupDebts(String groupId) {
        Group group = getGroup(groupId);
        if (group == null) return;

        // Use group's balance data for debt simplification
        group.simplifyGroupDebts();
    }
}

public class SplitwiseApp {
    public static void main(String[] args) {

        Splitwise manager = Splitwise.getInstance();

        System.out.println("\n=========== Creating Users ====================");
        User user1 = manager.createUser("Aditya", "aditya@gmail.com");
        User user2 = manager.createUser("Rohit", "rohit@gmail.com");
        User user3 = manager.createUser("Manish", "manish@gmail.com");
        User user4 = manager.createUser("Saurav", "saurav@gmail.com");

        System.out.println("\n=========== Creating Group and Adding Members ====================");
        Group hostelGroup = manager.createGroup("Hostel Expenses");
        manager.addUserToGroup(user1.userId, hostelGroup.groupId);
        manager.addUserToGroup(user2.userId, hostelGroup.groupId);
        manager.addUserToGroup(user3.userId, hostelGroup.groupId);
        manager.addUserToGroup(user4.userId, hostelGroup.groupId);

        System.out.println("\n=========== Adding Expenses in group ====================");    
        List<String> groupMembers = Arrays.asList(user1.userId, user2.userId, user3.userId, user4.userId);
        manager.addExpenseToGroup(hostelGroup.groupId, "Lunch", 800.0, user1.userId, groupMembers, SplitType.EQUAL);

        List<String> dinnerMembers = Arrays.asList(user1.userId, user3.userId, user4.userId);
        List<Double> dinnerAmounts = Arrays.asList(200.0, 300.0, 200.0);
        manager.addExpenseToGroup(hostelGroup.groupId, "Dinner", 700.0, user3.userId, dinnerMembers, 
                                 SplitType.EXACT, dinnerAmounts);

        System.out.println("\n=========== printing Group-Specific Balances ===================="); 
        manager.showGroupBalances(hostelGroup.groupId);

        System.out.println("\n=========== Debt Simplification ===================="); 
        manager.simplifyGroupDebts(hostelGroup.groupId);

        System.out.println("\n=========== printing Group-Specific Balances ===================="); 
        manager.showGroupBalances(hostelGroup.groupId);

        System.out.println("\n=========== Adding Individual Expense ===================="); 
        manager.addIndividualExpense("Coffee", 40.0, user2.userId, user4.userId, SplitType.EQUAL);

        System.out.println("\n=========== printing User Balances ===================="); 
        manager.showUserBalance(user1.userId);
        manager.showUserBalance(user2.userId);
        manager.showUserBalance(user3.userId);
        manager.showUserBalance(user4.userId);

        System.out.println("\n==========Attempting to remove Rohit from group==========");
        manager.removeUserFromGroup(user2.userId, hostelGroup.groupId);

        System.out.println("\n======== Making Settlement to Clear Rohit's Debt =========="); 
        manager.settlePaymentInGroup(hostelGroup.groupId, user2.userId, user3.userId, 200.0);

        System.out.println("\n======== Attempting to Remove Rohit Again ==========");
        manager.removeUserFromGroup(user2.userId, hostelGroup.groupId);

        System.out.println("\n=========== Updated Group Balances ===================="); 
        manager.showGroupBalances(hostelGroup.groupId);
    }
}

đź’ Debt Simplification Logic

As shown in the image — instead of multiple payments, we calculate net balances:

  • Calculate the net balance for each user (positive = you are owed money, negative = you owe money)

  • Create a list of creditors and debtors

  • Use a greedy algorithm to generate the minimum number of transactions

đź’ˇ Result? Optimized payments and zero confusion!

(Hindi:- Jaise image mein dikhaya gaya hai — instead of many payments, hum net balances calculate karte hain:

  1. Har user ka net balance nikalo (positive = tumhe paisa milna hai, negative = tumhe dena hai)

  2. Creditors aur Debtors list banao

  3. Greedy algorithm se minimum number of transactions generate karo

đź’ˇ Result? Optimized payments aur zero confusion! )

đź’ Real-World Use Case

Group: "Hostel Friends"

  • Expense 1: Lunch (Rs 800 by Aditya) split equally among 4

  • Expense 2: Dinner (Rs 700 by Manish) with custom amounts

  • Add individual expense: Rohit pays for coffee for Saurav

Users can:

  • View personal and group balances

  • Settle dues

  • Leave group only after settling dues

đź’ Why This Project Is Special?

  • Great LLD example for interviews

  • SOLID principle implementation

  • Realistic app architecture

  • Feature-rich with clear class structure

  • Includes group AND personal expenses

đź’ Final Thoughts

Yeh project ek perfect capstone hai agar aap system design, OOP, aur real-world product building seekhna chahte ho. Whether you're preparing for LLD/System Design Interviews or building your GitHub portfolio — this app will help you stand out!

Week - 7 (Day-1) Completed âś… System Design

NOTE : - A big thanks to my mentors Rohit Negi Sir and Aditya Sir for launching this amazing 8-week course absolutely free on YouTube via CoderArmy9 :- youtube.com/@CoderArmy9 . 🙌

👉 Share this blog with your connections! Let’s keep learning, growing, and supporting one another on this journey. 🚀

✍️ Payal Kumari 👩‍💻

Jai Hind 🇮🇳 | #CoderArmy #LearningInPublic #SystemDesign #TechForAll #MentorshipMatters #8weeksLLdChallenge #LowLevelDesign #LLD 👩‍💻

10
Subscribe to my newsletter

Read articles from Payal Kumari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Payal Kumari
Payal Kumari

I'm a passionate full-stack developer with a strong foundation in the MERN stack—building and maintaining scalable web applications using React.js, Node.js, and Next.js. My journey in open source began with Hacktoberfest 2023, where I made four impactful pull requests that sparked a love for collaborative coding, global learning, and open knowledge sharing. Since then, I’ve contributed to and mentored projects in top open source programs like GSSoC’24, SSOC’24, and C4GT’24. As a Google Gen AI Exchange Hackathon ’24 Finalist and Google’s Women Techmakers (WTM) Ambassador, I’ve been privileged to support diverse communities in building meaningful tech solutions. My work as a Top 50 Mentor for GSSoC ’24 reflects my commitment to nurturing new talent in tech. Beyond development, I serve as a Student Career Guide, Profile Building Expert & Evangelist at Topmate.io, where I conduct workshops, guide students through resume building and career strategy, and help mentees navigate open source and tech careers. Recognized among the Top 5% of mentors and featured on “Topmate Discover,” I take pride in making mentorship accessible and impactful. My technical voice has also been acknowledged by LinkedIn, where I’ve earned the Top Voice badge seven times in domains like web development, programming, and software engineering. In addition, I hold LinkedIn Golden Badges for Research Skills, Interpersonal Skills, Critical Thinking, and Teamwork—signaling a well-rounded approach to both individual contribution and team collaboration. Graduating with an MCA from Chandigarh University in 2023, I’ve continued to fuel my curiosity by writing technical articles and sharing practical MERN stack insights across platforms. Whether it’s building polished UIs, optimizing backend performance, or guiding a mentee through their first pull request, I’m driven by the power of community and continuous learning. Let’s connect! I'm open to collaborations, mentorship, or building something impactful together. Reach out to me at kumaripayal7488@gmail.com or visit my profile on Topmate.io.