Machine Coding Problem

Splitwise

maco30maco60macoAllfintechmin-cash-flowgraph-modeling
Commonly Asked By:UberMicrosoftFlipkartGrab

Functional Scope (In-Scope)

  • Pluggable Splitting Strategies: Supports interchangeable Equal, Exact, and Percentage calculations to divide bills dynamically.
  • Balance Graph Modeling: Keeps a single aggregated net balance grid (rather than storing redundant copy lines per expense).
  • Minimum Cash Flow Settlement: Solves the min-cash-flow transaction graph reduction problem to close debts optimally.
  • Precision Rounding Protection: Handles floating point fractions (e.g. splitting $10.00 equally between 3 users leaves a 1-cent remaining mismatch) by assigning offsets to one member dynamically.

Explicit Boundaries (Out-of-Scope)

  • No Real Currency Bank Integration: Ignores actual payment gateways or third-party bank webhooks.
  • No Dynamic Currency Swaps: Assumes a single unified functional currency for all calculations.

Production reference implementations showing robust split allocations and graph settlement algorithms in Java and Python:

// ─── JAVA BLUEPRINT ──────────────────────────────────────────────────────────
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;

class User {
    private final String id;
    private final String name;
    private final String email;

    public User(String id, String name, String email) {
        this.id = Objects.requireNonNull(id);
        this.name = Objects.requireNonNull(name);
        this.email = Objects.requireNonNull(email);
    }

    public String getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
}

enum SplitType { EQUAL, EXACT, PERCENT }

abstract class Split {
    private final User user;
    protected double amount;

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

    public User getUser() { return user; }
    public double getAmount() { return amount; }
    public void setAmount(double amount) { this.amount = amount; }
}

class EqualSplit extends Split {
    public EqualSplit(User user) { super(user); }
}

class ExactSplit extends Split {
    public ExactSplit(User user, double amount) {
        super(user);
        this.amount = amount;
    }
}

class PercentSplit extends Split {
    private final double percent;

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

    public double getPercent() { return percent; }
}

interface ExpenseSplitStrategy {
    void calculateAndValidate(List<Split> splits, double totalAmount);
}

class EqualSplitStrategy implements ExpenseSplitStrategy {
    @Override
    public void calculateAndValidate(List<Split> splits, double totalAmount) {
        int n = splits.size();
        if (n == 0) throw new IllegalArgumentException("Splits list cannot be empty.");

        double share = Math.floor((totalAmount / n) * 100.0) / 100.0;
        double totalCalculated = 0.0;
        for (Split split : splits) {
            split.setAmount(share);
            totalCalculated += share;
        }

        // Precision Rounding Protection
        double difference = totalAmount - totalCalculated;
        if (Math.abs(difference) > 0.001) {
            double adjustment = Math.round(difference * 100.0) / 100.0;
            splits.get(0).setAmount(splits.get(0).getAmount() + adjustment);
        }
    }
}

class ExactSplitStrategy implements ExpenseSplitStrategy {
    @Override
    public void calculateAndValidate(List<Split> splits, double totalAmount) {
        double totalSplitAmount = 0.0;
        for (Split split : splits) {
            totalSplitAmount += split.getAmount();
        }
        if (Math.abs(totalSplitAmount - totalAmount) > 0.01) {
            throw new IllegalArgumentException("Sum of exact splits (" + totalSplitAmount + ") must equal total amount (" + totalAmount + ").");
        }
    }
}

class PercentSplitStrategy implements ExpenseSplitStrategy {
    @Override
    public void calculateAndValidate(List<Split> splits, double totalAmount) {
        double totalPercent = 0.0;
        for (Split split : splits) {
            if (!(split instanceof PercentSplit)) {
                throw new IllegalArgumentException("Split must be of type PercentSplit");
            }
            totalPercent += ((PercentSplit) split).getPercent();
        }
        if (Math.abs(totalPercent - 100.0) > 0.01) {
            throw new IllegalArgumentException("Total percentage must equal 100%. Got: " + totalPercent);
        }

        double totalCalculated = 0.0;
        for (Split split : splits) {
            PercentSplit percentSplit = (PercentSplit) split;
            double share = Math.floor((totalAmount * percentSplit.getPercent() / 100.0) * 100.0) / 100.0;
            split.setAmount(share);
            totalCalculated += share;
        }

        // Precision Rounding Protection
        double difference = totalAmount - totalCalculated;
        if (Math.abs(difference) > 0.001) {
            double adjustment = Math.round(difference * 100.0) / 100.0;
            splits.get(0).setAmount(splits.get(0).getAmount() + adjustment);
        }
    }
}

class ExpenseSplitStrategyFactory {
    private static final Map<SplitType, ExpenseSplitStrategy> strategies = new HashMap<>();

    static {
        strategies.put(SplitType.EQUAL, new EqualSplitStrategy());
        strategies.put(SplitType.EXACT, new ExactSplitStrategy());
        strategies.put(SplitType.PERCENT, new PercentSplitStrategy());
    }

    public static ExpenseSplitStrategy getStrategy(SplitType type) {
        ExpenseSplitStrategy strategy = strategies.get(type);
        if (strategy == null) {
            throw new IllegalArgumentException("Invalid split type: " + type);
        }
        return strategy;
    }
}

class Expense {
    private final String id;
    private final String description;
    private final double amount;
    private final User paidBy;
    private final List<Split> splits;
    private final SplitType splitType;

    public Expense(String id, String description, double amount, User paidBy, List<Split> splits, SplitType splitType) {
        this.id = Objects.requireNonNull(id);
        this.description = Objects.requireNonNull(description);
        this.amount = amount;
        this.paidBy = Objects.requireNonNull(paidBy);
        this.splits = Objects.requireNonNull(splits);
        this.splitType = Objects.requireNonNull(splitType);
    }

    public String getId() { return id; }
    public String getDescription() { return description; }
    public double getAmount() { return amount; }
    public User getPaidBy() { return paidBy; }
    public List<Split> getSplits() { return splits; }
    public SplitType getSplitType() { return splitType; }
}

class SplitwiseService {
    private final Map<String, User> users = new ConcurrentHashMap<>();
    private final Map<String, Map<String, Double>> balanceSheet = new ConcurrentHashMap<>();
    private final List<Expense> expenses = new CopyOnWriteArrayList<>();
    private final ReentrantLock lock = new ReentrantLock();

    public void registerUser(User user) {
        users.put(user.getId(), user);
    }

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

    public void addExpense(String description, double amount, String paidById, List<Split> splits, SplitType splitType) {
        User paidBy = users.get(paidById);
        if (paidBy == null) {
            throw new IllegalArgumentException("User not registered: " + paidById);
        }

        // Resolve split strategy and calculate exact share values
        ExpenseSplitStrategy strategy = ExpenseSplitStrategyFactory.getStrategy(splitType);
        strategy.calculateAndValidate(splits, amount);

        lock.lock();
        try {
            String expenseId = "EXP-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
            Expense expense = new Expense(expenseId, description, amount, paidBy, splits, splitType);
            expenses.add(expense);

            for (Split split : splits) {
                String oweUserId = split.getUser().getId();
                if (oweUserId.equals(paidById)) continue;

                double shareAmount = split.getAmount();

                // paidBy lent shareAmount to oweUserId
                updateBalance(paidById, oweUserId, shareAmount);
                updateBalance(oweUserId, paidById, -shareAmount);
            }
            System.out.println("[Splitwise] Added expense: '" + description + "' of $" + amount + " paid by " + paidBy.getName());
        } finally {
            lock.unlock();
        }
    }

    private void updateBalance(String userA, String userB, double amount) {
        balanceSheet.computeIfAbsent(userA, k -> new ConcurrentHashMap<>());
        Map<String, Double> balances = balanceSheet.get(userA);
        balances.put(userB, balances.getOrDefault(userB, 0.0) + amount);
    }

    public List<String> settleUp() {
        lock.lock();
        try {
            Map<String, Double> netBalances = new HashMap<>();

            // Aggregate global net positions
            for (String userA : balanceSheet.keySet()) {
                for (Map.Entry<String, Double> entry : balanceSheet.get(userA).entrySet()) {
                    netBalances.put(userA, netBalances.getOrDefault(userA, 0.0) + entry.getValue());
                }
            }

            // Max Heap for Creditors, Min Heap for Debtors
            PriorityQueue<Map.Entry<String, Double>> debtors = new PriorityQueue<>(
                Comparator.comparingDouble(Map.Entry::getValue)
            );
            PriorityQueue<Map.Entry<String, Double>> creditors = new PriorityQueue<>(
                (a, b) -> Double.compare(b.getValue(), a.getValue())
            );

            for (Map.Entry<String, Double> entry : netBalances.entrySet()) {
                double val = entry.getValue();
                if (val < -0.01) {
                    debtors.add(new AbstractMap.SimpleEntry<>(entry.getKey(), val));
                } else if (val > 0.01) {
                    creditors.add(new AbstractMap.SimpleEntry<>(entry.getKey(), val));
                }
            }

            List<String> transactions = new ArrayList<>();
            while (!debtors.isEmpty() && !creditors.isEmpty()) {
                Map.Entry<String, Double> debtor = debtors.poll();
                Map.Entry<String, Double> creditor = creditors.poll();

                double debtAmount = -debtor.getValue();
                double creditAmount = creditor.getValue();
                double settledValue = Math.min(debtAmount, creditAmount);

                User debtorUser = users.get(debtor.getKey());
                User creditorUser = users.get(creditor.getKey());

                transactions.add(debtorUser.getName() + " pays " + creditorUser.getName() + " $" + String.format("%.2f", settledValue));

                double remainingDebtor = debtor.getValue() + settledValue;
                double remainingCreditor = creditor.getValue() - settledValue;

                if (remainingDebtor < -0.01) {
                    debtor.setValue(remainingDebtor);
                    debtors.add(debtor);
                }
                if (remainingCreditor > 0.01) {
                    creditor.setValue(remainingCreditor);
                    creditors.add(creditor);
                }
            }
            return transactions;
        } finally {
            lock.unlock();
        }
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        SplitwiseService service = new SplitwiseService();

        User alice = new User("U1", "Alice", "alice@example.com");
        User bob = new User("U2", "Bob", "bob@example.com");
        User charlie = new User("U3", "Charlie", "charlie@example.com");
        User david = new User("U4", "David", "david@example.com");

        service.registerUser(alice);
        service.registerUser(bob);
        service.registerUser(charlie);
        service.registerUser(david);

        System.out.println("--- Registering expenses ---");

        // 1. Alice paid $100 for Equal split among Alice, Bob, Charlie, David
        List<Split> splits1 = Arrays.asList(
            new EqualSplit(alice),
            new EqualSplit(bob),
            new EqualSplit(charlie),
            new EqualSplit(david)
        );
        service.addExpense("Dinner", 100.00, "U1", splits1, SplitType.EQUAL);

        // 2. Bob paid $50 for Exact split: Charlie owes $10, David owes $40
        List<Split> splits2 = Arrays.asList(
            new ExactSplit(charlie, 10.00),
            new ExactSplit(david, 40.00)
        );
        service.addExpense("Cab ride", 50.00, "U2", splits2, SplitType.EXACT);

        // 3. Charlie paid $120.00 for Percentage split: Alice (20%), Bob (30%), Charlie (50%)
        List<Split> splits3 = Arrays.asList(
            new PercentSplit(alice, 20.0),
            new PercentSplit(bob, 30.0),
            new PercentSplit(charlie, 50.0)
        );
        service.addExpense("Groceries", 120.00, "U3", splits3, SplitType.PERCENT);

        System.out.println("\n--- Calculating Debt Simplification (Min Cash Flow) ---");
        List<String> plans = service.settleUp();
        for (String plan : plans) {
            System.out.println("  -> " + plan);
        }

        System.out.println("\n--- Simulating Concurrent Expense Additions ---");
        ExecutorService executor = Executors.newFixedThreadPool(3);
        Runnable t1 = () -> service.addExpense("Coffee", 15.00, "U1", Arrays.asList(new EqualSplit(alice), new EqualSplit(bob)), SplitType.EQUAL);
        Runnable t2 = () -> service.addExpense("Snacks", 25.00, "U2", Arrays.asList(new EqualSplit(bob), new EqualSplit(charlie)), SplitType.EQUAL);

        executor.submit(t1);
        executor.submit(t2);
        executor.shutdown();
        executor.awaitTermination(2, TimeUnit.SECONDS);

        System.out.println("\n--- New Debt Plan After Concurrency ---");
        plans = service.settleUp();
        for (String plan : plans) {
            System.out.println("  -> " + plan);
        }
    }
}