We now have a complete financial truth layer: transactions, transfers, splits, reconciliation, rules, undo, and import. But truth alone isn't enough. Users don't just want to know what happenedβthey want to express what they intended to happen.
This sprint introduces budgetsβour first venture into the intent layer. And the key insight is that intent must never try to become truth.
The Observer Pattern
The critical design decision for this sprint: budgets observe reality, they don't enforce it.
Most budgeting apps get this wrong. They treat budgets as enforcement mechanismsβblocking transactions, showing angry red warnings, making users feel guilty. This creates two problems:
- Reality wins anyway. You can't actually stop someone from spending money. The transaction already happened. Your credit card doesn't care about your grocery budget.
- Judgment doesn't help. Making users feel bad about overspending doesn't help them make better decisions. It just makes them avoid the app.
Our budgets take a different approach. They're purely observational. They show you what you planned, what you spent, and the difference. That's information, not judgment.
"Budgets show progress, not failure. Spending over budget isn't an errorβit's information."
Derived, Not Stored
Every budget calculation is derived on-demand from transactions. We don't store aggregates. This might seem inefficient, but it's essential for several reasons:
- Undo safety: If you undo a transaction, the budget immediately reflects the change. No stale data.
- Time travel: You can calculate budget status for any historical period by replaying transactions.
- Determinism: Same inputs always produce same outputs. Critical for sync.
- Simplicity: One source of truth (transactions). Budgets just observe.
// All these values are computed, never stored interface BudgetCalculation { budgetedAmount: number; // From budget.amount spentAmount: number; // Sum of period expenses remainingAmount: number; // budgetedAmount - spentAmount rolloverAmount: number; // Derived from prior periods effectiveBudget: number; // budgetedAmount + rolloverAmount effectiveRemaining: number; // effectiveBudget - spentAmount }
Period Handling Done Right
Dates are hard. Timezones are harder. Calendar math is hardest. We addressed this by:
- UTC everywhere: All period calculations use UTC. No timezone ambiguity.
- Pure functions:
getPeriodStart,getPeriodEnd,getNextPeriodare deterministic. - ISO weeks: Weekly budgets use ISO week definition (Monday start).
- Explicit boundaries: Every period has a start timestamp and end timestamp. No implicit assumptions.
// Period boundaries are explicit getPeriodStart(date, 'monthly') // First day of month, 00:00:00.000 UTC getPeriodEnd(date, 'monthly') // Last day of month, 23:59:59.999 UTC // Period keys are human-readable getPeriodKey(date, 'monthly') // "2024-01" getPeriodKey(date, 'weekly') // "2024-W03" getPeriodKey(date, 'yearly') // "2024"
Rollover: Intent Carrying Forward
When rollover is enabled, unspent budget carries forward to the next period. If you overspend, that deficit carries forward too. The key insight: rollover is derived, not stored.
To calculate rollover for the current period, we walk through all periods from when the budget started:
async calculateRollover(budget, currentPeriodStart) {
let rollover = 0;
let period = budget.startDate;
while (period < currentPeriodStart) {
const spent = await getCategorySpending(period);
rollover = budget.amount - spent + rollover;
period = getNextPeriodStart(period);
}
return rollover;
}This seems expensive, but it's actually cheap (few database queries), and more importantly, it's correct. If you undo a transaction from three months ago, the rollover for all subsequent months automatically updates.
Undo Preserves Intent
Budget operations emit to the operation log just like everything else:
-
budget.createβ Created a new budget -
budget.updateβ Changed amount or settings -
budget.deleteβ Removed a budget
And they can all be undone:
-
budget.revertβ Undo creation (soft delete) -
budget.revert.updateβ Restore previous values -
budget.restoreβ Undo deletion (undelete)
The critical insight: undoing a budget change never affects transactions. Intent and truth remain separate layers. You can't accidentally delete your transactions by undoing a budget.
The Architecture of Intent
With budgets, we've established a new architectural pattern:
βββββββββββββββββββββββββββββββββββββββββββββββ
β INTENT LAYER β
β Budgets, Goals, Plans (observational) β
βββββββββββββββββββββββββββββββββββββββββββββββ
β
β observes (never modifies)
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββ
β TRUTH LAYER β
β LedgerService (authoritative) β
βββββββββββββββββββββββββββββββββββββββββββββββThis separation is fundamental. Future features like goals, savings targets, and spending forecasts will all live in the intent layer. They'll all observe truth without trying to become it.
What We Built
Budget Primitive
Category, period, amount, rollover settings.
BudgetService
CRUD, operation logging, and derived calculations.
Period Utilities
Monthly/weekly/yearly boundaries, deterministic.
Rollover Calculation
Derived from prior periods, automatically updates.
Full Undo Support
All budget operations are reversible via compensating ops.
Test Suite
Period calculations, spending, rollover, and undo tests.
The Principle
Intent should never rewrite truth.
Your budget says $500 for groceries. You spent $600. The budget doesn't change the $600 to $500. It doesn't hide the difference. It shows you reality and trusts you to make decisions with that information.
This is what local-first means for intent: your plans live alongside your data, fully offline, fully undoable, but never trying to override what actually happened.
Sprint 16 Checklist
// Sprint 16 Verification β Budget primitive exists and is documented β All calculations are derived only β Rollover works correctly β Undo works for budget changes β Budget UI is non-judgmental β Regression suite passes β Day 16 Dev Journal is present ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ All checklist items complete ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Budgets used to be about control. Now they're about awareness. No blocking. No judgment. Just information about what you intended versus what happened.
Your intent. Your truth. Both respected.
β The Accelerate Finance Team