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