Today we defined what "financial truth" means for Accelerate Finance. No new features. No UI polish. Just correctness. After this sprint, money movement is unambiguous, auditable, and AI-readable.
The Core Problem
Financial software has a fundamental question to answer: Where does truth live?
In Sprint 2, we had a subtle bug. The currentBalance field on accounts was stored directly—but what if it drifted from reality? What if a transaction update didn't trigger a recalculation? The balance would lie.
Today we fixed that with a simple rule:
Account Balance = initialBalance + sum(all transactions)
Always. No exceptions. Derived, not stored.
The LedgerService: Domain Boundary
We introduced a LedgerService—the single source of truth for all financial operations. This is not just a helper. It's a domain boundary.
What LedgerService Does
- + Creates transactions (income, expense)
- + Creates transfers (two linked transactions)
- + Creates split transactions (one parent, multiple categorized children)
- + Calculates derived balances (always authoritative)
- + Manages reconciliation state
- + Syncs balance caches (so UI stays fast)
What Repositories Do NOT Do
- - Compute balances (that's business logic)
- - Enforce transfer invariants
- - Validate split sums
- - Any financial logic—just persistence
How Transfers Work
A transfer is money moving between accounts. The question: how do you represent this?
We chose the two-transaction model:
- One transaction leaves the source account (negative amount)
- One transaction enters the destination (positive amount)
- Both share a
transferGroupId - Deleting one deletes both (enforced by LedgerService)
This means transfers never double-count. Each account's balance is correct independently. The ledger sums correctly whether you look at one account or all accounts.
How Splits Work
A split transaction breaks one payment into multiple category allocations. Example: a $100 grocery run that's 60% food, 40% household supplies.
// Parent transaction (holds the total) { amount: -10000, // -$100 splitGroupId: "abc123", splitParentId: null, // This IS the parent categoryId: null // No category on parent } // Child 1: Groceries { amount: -6000, // -$60 splitGroupId: "abc123", splitParentId: "parent-id", categoryId: "groceries-id" } // Child 2: Household { amount: -4000, // -$40 splitGroupId: "abc123", splitParentId: "parent-id", categoryId: "household-id" }
Key invariant: sum(children) must equal parent. The LedgerService enforces this at creation time.
Why Reconciliation Exists
Reconciliation is the process of verifying transactions against your bank statement. It answers: "Does my ledger match reality?"
We added a reconciledAt timestamp to every transaction:
-
null= unreconciled (not yet verified) - Timestamp = when it was marked as verified
Reconciliation does NOT affect balance calculation. All transactions count. But reconciliation gives you:
- Confidence — "These transactions are real, not typos"
- Audit trail — "I verified this on Dec 20"
- Import safety — "Don't duplicate reconciled transactions"
The Account Model Correction
We kept currentBalance on the Account model, but with explicit documentation:
currentBalance is a CACHE
It exists for performance. The LedgerService is the authoritative source. If the cache drifts, recalculate it. The UI shows cached values for speed, but the ledger calculates truth.
We added auditWorkspaceBalances() to detect and fix drift—a diagnostic tool for debugging.
Explicit Enums
Every "type" field now has an explicit enum with documented semantics:
Plus helper functions: isAssetAccount(), isLiabilityAccount() for net worth calculations.
Ledger Invariants
The LedgerService guarantees these invariants:
1. Account balance = initialBalance + sum(transactions)
2. Transfers create exactly two transactions with equal absolute amounts
3. Split children sum to parent amount
4. No orphan transactions (every tx has valid account)
5. No floating point—all amounts are integers (cents)
6. Deleting a transfer deletes both sides
What We Built Today
- LedgerService — Domain boundary for all financial operations
- Transaction refinements — payee field, transferGroupId, splitGroupId, reconciledAt
- Transfer support — Two linked transactions that move together
- Split support — Parent/child model with category allocation
- Reconciliation — Mark/unmark transactions as verified
- Explicit enums — Type-safe, documented type fields
- Ledger UI — Functional view with date filtering, reconciliation toggle, edit/delete
Financial truth is now defined. Balances are derived, not stored. Transfers don't double-count. Splits sum correctly. Reconciliation persists. The ledger works offline. No repository contains business logic.
This foundation means we can trust the numbers. AI can trust the numbers. Users can trust the numbers.
— The Accelerate Finance Team