Machine Coding Problem

Expense Tracker

maco60macoAllfintechaggregationcategory-strategy
Commonly Asked By:RobinhoodBlockIntuit

Requirements & System Scope

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).

Class Diagram & Entity Relationships

Structural layout showing relationships between transactions, category budgets, and service modules:

Loading...
  • Transactional Logs: Transaction stores basic payment data alongside assigned category tags.
  • Budget Monitors: Budget pairs category limits with volatile rolling spent counts.

Design Patterns & SOLID Principles

  • Strategy Pattern (Auto-Categorization): Decoupling category logic (Regex checkers, keyword lists) into a pluggable strategy allows new categorization rules to be added easily, satisfying Open-Closed Principle.
  • Observer Pattern (Budget Warnings): Registering notification and telemetry services as observers enables dynamic triggers (like warnings at 85% and alerts at 100%) without coupling billing engines directly to notifier logic.

Core Execution Workflows

Transaction Posting & Budget Review

  1. Buyer registers new expense: addTransaction(amount, merchant).
  2. Pass transaction details through pluggable category strategies to auto-classify categories.
  3. Append transaction records to persistent memory lists.
  4. Query budget monitors for the assigned category:
    1. If a budget exists, update the spent count.
    2. Compare totals: if spent amount exceeds the warning threshold (85%) or limit (100%), notify registered observers.

Concurrency & Thread Safety Strategy

When multiple threads add transactions or query budget statuses concurrently:

  • Concurrent Collections: Store transactions in thread-safe collections (e.g., CopyOnWriteArrayList) to allow parallel read lookups during updates.
  • Synchronized Budget Updates: Use locks inside budget monitors to prevent race conditions during spent updates.

Complete Clean Code Blueprint

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()));
        }
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...