Machine Coding Problem

Expense Tracker

maco60macoAllfintechaggregationcategory-strategy
Commonly Asked By:RobinhoodBlockIntuit

Functional Scope (In-Scope)

  • Categorized Expense Records: Create transaction logs supporting metadata details like amount, merchant info, and target date.
  • Dynamic Category Assignment Rules: Auto-categorize entries by parsing merchant keywords or allow manual override tags.
  • Monthly Budget Alarms: Monitor running costs and trigger warning alarms when monthly budgets are exceeded using the Observer Pattern.
  • Expense Aggregators: Aggregate historical transaction lists into monthly sums and category breakdown lists.

Explicit Boundaries (Out-of-Scope)

  • No Real-World Bank Integration: Bypasses live bank API scrapers or OCR processing for physical receipts.
  • No Multi-User Split Accounting: Excludes complex multi-user bill splitting or debt settlement pools (handled in Splitwise).

Clean reference designs demonstrating budget alarms in Java and Python:

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

enum Category { FOOD, TRAVEL, SHOPPING, UTILITIES, OTHERS }

class Transaction {
    private final String id;
    private final double amount;
    private final Date date;
    private final String merchant;
    private Category category;

    public Transaction(String id, double amount, Date date, String merchant) {
        this.id = id;
        this.amount = amount;
        this.date = date;
        this.merchant = merchant;
        this.category = Category.OTHERS;
    }

    public String getId() { return id; }
    public double getAmount() { return amount; }
    public Date getDate() { return date; }
    public String getMerchant() { return merchant; }
    public Category getCategory() { return category; }
    public void setCategory(Category category) { this.category = category; }
}

// Strategy Pattern for Auto-Categorization
interface CategorizationStrategy {
    Category categorize(String merchant);
}

class MerchantKeywordStrategy implements CategorizationStrategy {
    private final Map<String, Category> keywords = new ConcurrentHashMap<>();

    public MerchantKeywordStrategy() {
        keywords.put("uber", Category.TRAVEL);
        keywords.put("lyft", Category.TRAVEL);
        keywords.put("starbucks", Category.FOOD);
        keywords.put("mcdonalds", Category.FOOD);
        keywords.put("walmart", Category.SHOPPING);
        keywords.put("amazon", Category.SHOPPING);
        keywords.put("power", Category.UTILITIES);
        keywords.put("water", Category.UTILITIES);
    }

    @Override
    public Category categorize(String merchant) {
        String query = merchant.toLowerCase();
        for (Map.Entry<String, Category> entry : keywords.entrySet()) {
            if (query.contains(entry.getKey())) {
                return entry.getValue();
            }
        }
        return Category.OTHERS;
    }
}

// Observer Pattern for Budget Notifications
interface BudgetObserver {
    void onThresholdBreached(Category category, double spent, double limit, double percentage);
}

class UserAlertNotificationService implements BudgetObserver {
    @Override
    public void onThresholdBreached(Category category, double spent, double limit, double percentage) {
        if (percentage >= 100.0) {
            System.out.println("[ALERT CRITICAL] Budget for " + category + " EXCEEDED! Spent: $" 
                               + String.format("%.2f", spent) + " / Limit: $" + limit);
        } else {
            System.out.println("[ALERT WARNING] Budget for " + category + " is approaching limit (" 
                               + String.format("%.1f", percentage) + "% spent). Spent: $" 
                               + String.format("%.2f", spent) + " / Limit: $" + limit);
        }
    }
}

class Budget {
    private final Category category;
    private final double limit;
    private double spent = 0.0;
    private final ReentrantLock lock = new ReentrantLock();

    public Budget(Category category, double limit) {
        this.category = category;
        this.limit = limit;
    }

    public Category getCategory() { return category; }
    public double getLimit() { return limit; }
    public double getSpent() { return spent; }

    public double addExpense(double amount) {
        lock.lock();
        try {
            this.spent += amount;
            return this.spent;
        } finally {
            lock.unlock();
        }
    }
}

class ExpenseService {
    private final List<Transaction> transactions = new CopyOnWriteArrayList<>();
    private final Map<Category, Budget> budgets = new ConcurrentHashMap<>();
    private final List<BudgetObserver> observers = new CopyOnWriteArrayList<>();
    private CategorizationStrategy categorizationStrategy = new MerchantKeywordStrategy();

    public void registerObserver(BudgetObserver obs) {
        observers.add(obs);
    }

    public void setCategorizationStrategy(CategorizationStrategy strategy) {
        this.categorizationStrategy = strategy;
    }

    public void setBudget(Category category, double limit) {
        budgets.put(category, new Budget(category, limit));
    }

    public void addTransaction(double amount, String merchant) {
        String txId = UUID.randomUUID().toString().substring(0, 8);
        Transaction tx = new Transaction(txId, amount, new Date(), merchant);
        
        // Auto-categorize
        Category cat = categorizationStrategy.categorize(merchant);
        tx.setCategory(cat);
        transactions.add(tx);

        Budget budget = budgets.get(cat);
        if (budget != null) {
            double currentSpent = budget.addExpense(amount);
            double percentage = (currentSpent * 100.0) / budget.getLimit();
            
            // Check warnings (at 85% or 100%)
            if (percentage >= 85.0) {
                notifyObservers(cat, currentSpent, budget.getLimit(), percentage);
            }
        }
    }

    private void notifyObservers(Category cat, double spent, double limit, double percentage) {
        for (BudgetObserver observer : observers) {
            observer.onThresholdBreached(cat, spent, limit, percentage);
        }
    }

    public List<Transaction> getTransactions() { return new ArrayList<>(transactions); }

    public Map<Category, Double> getCategorySummaries() {
        Map<Category, Double> summaries = new HashMap<>();
        for (Transaction tx : transactions) {
            summaries.put(tx.getCategory(), summaries.getOrDefault(tx.getCategory(), 0.0) + tx.getAmount());
        }
        return summaries;
    }
}

// Complete Simulation
public class Main {
    public static void main(String[] args) {
        ExpenseService service = new ExpenseService();
        service.registerObserver(new UserAlertNotificationService());

        // Set monthly budgets
        service.setBudget(Category.FOOD, 100.0);
        service.setBudget(Category.TRAVEL, 150.0);

        System.out.println("--- Registering Transactions ---");
        service.addTransaction(12.50, "Starbucks Coffee");
        service.addTransaction(45.00, "Uber Premium");
        service.addTransaction(75.00, "Mcdonalds Family Meal"); // Total food = 87.50 (warning expected)
        service.addTransaction(20.00, "Starbucks Snack");       // Total food = 107.50 (critical expected)

        System.out.println("\n--- Expense Summary breakdown ---");
        Map<Category, Double> summary = service.getCategorySummaries();
        for (Map.Entry<Category, Double> entry : summary.entrySet()) {
            System.out.println(entry.getKey() + ": $" + String.format("%.2f", entry.getValue()));
        }
    }
}