In a standard CRUD (Create, Read, Update, Delete) application, editing a record is usually the simplest feature to implement.
If a user edits a blog post title, you execute UPDATE posts SET title = ? WHERE id = ?. If they update their username, you overwrite the database column. The change is isolated, self-contained, and has zero side effects on other rows.
In a personal finance ledger, however, there is no such thing as an isolated edit.
A transaction is not just a row of text; it is an economic event that previously altered account balances, shifted carried-forward equity, updated budget burn rates, and possibly affected credit card billing statements.
If your application naively performs an in-place SQL update on a transaction record, your ledger balances will immediately corrupt.
Here is how FinLedger architected safe transaction editing using an atomic two-phase posting reconciliation engine.
The In-Place Update Trap
To understand why in-place updates fail, consider what happens when a user modifies a simple expense.
Suppose on October 5th, a user enters a 2,000-rupee grocery expense paid from their HDFC Bank checking account. At that moment:
- HDFC checking balance is debited by 2,000 rupees.
- Monthly grocery spending increases by 2,000 rupees.
Three days later, the user realizes they made a mistake: the bill was actually 2,500 rupees, and they paid with cash from their physical wallet, not their HDFC debit card.
flowchart TD
subgraph NaiveUpdate["The Naive In-Place SQL Update"]
direction TB
E1["User Changes: Amount (2,000 -> 2,500) & Account (HDFC -> Cash)"] --> E2["Run In-Place SQL:
UPDATE transactions SET amount = 2500, account_id = 'cash' WHERE id = 101"]
E2 --> E3["Result:
HDFC Account STILL MISSING 2,000!
Cash Account NEVER DEBITED!
Ledger Corrupted Forever!"]
end
If the developer simply runs UPDATE transactions SET amount = 2500, account = 'cash', what happens to the account balances?
- The HDFC checking account is still missing the original 2,000 rupees that was deducted on October 5th.
- The Cash wallet was never debited by the new 2,500-rupee amount.
- The database is now out of sync with physical reality.
Even if the developer writes a custom helper to calculate the difference between the old amount and the new amount ($2,500 - 2,000 = +500$), that delta arithmetic breaks down the moment the user edits the account, changes the transaction type (e.g. from an Expense to a Transfer), or changes the date across a monthly statement boundary.
The Two-Phase Reconciliation Solution
To ensure absolute mathematical consistency, FinLedger treats every transaction edit as a formal two-phase financial reconciliation.
Rather than attempting to calculate complex multi-variable deltas between the old and new states, the repository executes a clean inverse-then-post sequence:
flowchart LR
subgraph EditCycle["Two-Phase Invariant Pipeline"]
direction TB
OldTx["1. Fetch Stored Original Transaction"] --> Inverse["Phase 1: Invert Original Postings
(Refund original accounts)"]
NewTx["2. Validate New Transaction Inputs"] --> Forward["Phase 2: Apply New Postings
(Debit/Credit updated accounts)"]
Inverse & Forward --> Atomic["Execute Atomically in SQLite:
AppDatabase.withTransaction { ... }"]
end
Phase 1: Inverse Unwinding
- The engine retrieves the existing, unedited transaction entity from the database.
- It feeds the original transaction through
Posting.ktto determine its exact original impact on account balances. - It inverts those postings: if the original transaction deducted 2,000 rupees from HDFC checking, the engine adds 2,000 rupees back to HDFC checking.
- At this millisecond, the ledger state is restored to exactly what it would have been if the original transaction had never occurred.
Phase 2: Forward Application
- The engine validates the newly edited transaction data (ensuring positive non-zero amounts, distinct transfer accounts, and valid categories).
- It feeds the new transaction through
Posting.ktto compute the new balance deltas (e.g., deducting 2,500 rupees from the Cash wallet). - It updates the transaction row in the SQLite database.
- It recalculates the final balances for all touched accounts (both the old accounts that were refunded and the new accounts that were debited).
Guaranteeing ACID Atomicity via withTransaction
The critical requirement of this two-phase sequence is atomicity.
If the application unwinds Phase 1 (restoring 2,000 rupees to HDFC), and the Android operating system terminates the app process or the device battery dies before Phase 2 completes, the user will be left with phantom money.
In FinLedger, the entire lifecycle is encapsulated inside Room's transactional block:
flowchart TD
subgraph RoomTransaction["SQLite Transaction Boundary (withTransaction)"]
direction TB
Step1["1. Begin SQLite Transaction (BEGIN EXCLUSIVE)"] --> Step2["2. Revert Old Postings on Touched Accounts"]
Step2 --> Step3["3. Update Transaction Table Row"]
Step3 --> Step4["4. Apply New Postings on Touched Accounts"]
Step4 --> Step5["5. Synchronize Companion Records (Lending / Receipts)"]
Step5 --> Step6["6. Commit Transaction (COMMIT)"]
end
Step6 -->|"On Any Error / Exception"| Rollback["Automatic SQLite ROLLBACK (Zero Data Loss)"]
If an exception occurs at any point—whether due to a database constraint violation, invalid input, or unexpected process interruption—SQLite automatically executes a full rollback. Either both phases succeed completely, or the database reverts to its exact pre-edit state.
Date Boundary Edits and Statement Snapshots
A further complication arises when the transaction's date is edited across billing boundaries:
- If a credit card transaction dated October 10th (billed in the October statement) is edited to October 18th (belonging to the November statement), the credit card billing engine must recalculate statement balances for both cycles.
- FinLedger's credit card statement engine re-evaluates closing timestamp boundaries dynamically, reallocating waterfall payments so that past closed bills remain immutable snapshots while current statements reflect the edited date.
Key Takeaway
Editing financial records requires respecting historical cause and effect.
Never rely on in-place field updates for financial data. By cleanly unwinding the original transaction's postings and applying the updated transaction's postings inside an atomic SQLite transaction, your ledger preserves 100% balance integrity across every edit.
FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.
Explore FinLedger on Google Play: https://play.google.com/store/apps/details?id=com.devanshufinance.app

Top comments (0)