<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: DEVANSHU PATIL</title>
    <description>The latest articles on DEV Community by DEVANSHU PATIL (@devanshu_patil).</description>
    <link>https://dev.to/devanshu_patil</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4112603%2F2dbe1932-ed99-4536-bdd7-f380a1fbe42d.png</url>
      <title>DEV Community: DEVANSHU PATIL</title>
      <link>https://dev.to/devanshu_patil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devanshu_patil"/>
    <language>en</language>
    <item>
      <title>How to Actually Audit Your Spending Without Spiraling Into Anxiety</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Sat, 07 Nov 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/how-to-actually-audit-your-spending-without-spiraling-into-anxiety-5d2f</link>
      <guid>https://dev.to/devanshu_patil/how-to-actually-audit-your-spending-without-spiraling-into-anxiety-5d2f</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnew3jt0tk1nkl6v4ucq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnew3jt0tk1nkl6v4ucq4.png" alt="How to Actually Audit Your Spending Without Spiraling Into Anxiety" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most people dread personal financial audits.&lt;/p&gt;

&lt;p&gt;The traditional advice sounds exhausting: download three months of PDF bank statements, open a spreadsheet, manually copy every single line item into Excel, create pivot tables, and spend six hours on a Sunday afternoon squinting at numbers.&lt;/p&gt;

&lt;p&gt;After doing that once, 95% of people never do it again. It feels like doing homework for your own life.&lt;/p&gt;

&lt;p&gt;The problem is not that financial audits are unnecessary; the problem is that traditional auditing methods focus on the wrong details.&lt;/p&gt;

&lt;p&gt;You do not need to analyze every 50-rupee chai or individual taxi ride to take control of your financial health. You need a structured, low-friction framework that answers three high-impact questions in less than twenty minutes.&lt;/p&gt;

&lt;p&gt;Here is the three-tier financial audit framework, and how modern offline tools make it painless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three-Tier Audit Framework
&lt;/h2&gt;

&lt;p&gt;A productive personal financial review does not examine all transactions equally. It operates through three progressive filters:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph AuditFramework["The 3-Tier Financial Audit Framework"]
        direction TB
        Tier1["Tier 1: Balance Reconciliation (5 Minutes)
Compare bank statement ending balances against ledger balances. Fix discrepancies."]
        Tier2["Tier 2: Subscription &amp;amp; Recurring Pruning (5 Minutes)
Audit automated monthly recurring charges. Cancel zombie services."]
        Tier3["Tier 3: Category Burn Rate Review (10 Minutes)
Review top 3 spending categories. Check if reality matched your intentions."]
    end
    Tier1 --&amp;gt; Tier2 --&amp;gt; Tier3&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Tier 1: Balance Reconciliation (5 Minutes)
&lt;/h3&gt;

&lt;p&gt;Before looking at where your money went, you must confirm that you know where your money is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open your primary banking app and check your current balance.&lt;/li&gt;
&lt;li&gt;Open your personal finance app and compare the numbers.&lt;/li&gt;
&lt;li&gt;If they match down to the paisa, you have total confidence in your data.&lt;/li&gt;
&lt;li&gt;If there is a small discrepancy, use FinLedger's &lt;strong&gt;Bank Statement Reconciliation&lt;/strong&gt; tool to record an adjustment (such as a bank service fee or interest credit) rather than trying to hunt down missing receipts from six weeks ago.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Tier 2: Subscription &amp;amp; Recurring Pruning (5 Minutes)
&lt;/h3&gt;

&lt;p&gt;Recurring charges are the silent killers of personal wealth. A 400-rupee streaming service, a 999-rupee software subscription, or a 1,500-rupee forgotten gym membership quietly siphon thousands of rupees every year because they require zero active approval.&lt;/p&gt;

&lt;p&gt;In FinLedger, the &lt;strong&gt;Subscriptions Hub&lt;/strong&gt; uses an automated on-device pattern detector (&lt;code&gt;SubscriptionDetectorEngine.kt&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It scans historical transactions for recurring patterns ($\ge 2$ occurrences with consistent intervals and amounts).&lt;/li&gt;
&lt;li&gt;It highlights active subscriptions with their annual cost projections.&lt;/li&gt;
&lt;li&gt;In five minutes, ask yourself: &lt;em&gt;"Did I use this service in the last 30 days?"&lt;/em&gt; If not, cancel the subscription immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tier 3: Category Burn Rate Review (10 Minutes)
&lt;/h3&gt;

&lt;p&gt;Instead of agonizing over 50 tiny expenses, focus exclusively on your &lt;strong&gt;top 3 spending categories&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For most people, 80% of discretionary spending concentrates in three areas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dining out and food delivery&lt;/li&gt;
&lt;li&gt;Shopping and impulse online purchases&lt;/li&gt;
&lt;li&gt;Travel and weekend entertainment
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph CategoryInsights["Category Donut Breakdown"]
        direction LR
        Spend["Total Monthly Outflow: 45,000"]
        Top1["1. Food &amp;amp; Dining: 18,000 (40%)"]
        Top2["2. Online Shopping: 12,000 (26.6%)"]
        Top3["3. Fuel &amp;amp; Commute: 6,000 (13.3%)"]
        Other["All Other Categories: 9,000 (20%)"]

        Spend --&amp;gt; Top1 &amp;amp; Top2 &amp;amp; Top3 &amp;amp; Other
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Open your monthly summary and look at the &lt;strong&gt;Category Donut Breakdown&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Look at the actual rupee total spent in your top category.&lt;/li&gt;
&lt;li&gt;Compare it against your monthly budget.&lt;/li&gt;
&lt;li&gt;Ask a single question: &lt;em&gt;"Did this spending align with my real values and priorities this month?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer is yes, celebrate the fact that your money served your life. If the answer is no, you have identified the exact single behavioral change to make next month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Date-Grouped Ledgers Beat Raw Spreadsheets
&lt;/h2&gt;

&lt;p&gt;Spreadsheets are passive grids of data. A date-grouped mobile ledger is an interactive narrative.&lt;/p&gt;

&lt;p&gt;In FinLedger:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Monthly Summary&lt;/strong&gt; groups transactions by day, showing net daily cash flow. You can instantly spot "expensive days" (like an expensive Saturday night out) and understand what drove the spike.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Available Spendable Money&lt;/strong&gt; hero card reminds you how much liquid cash you have left today after cordoning off money for upcoming credit card bills.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Making the Audit a Habit
&lt;/h2&gt;

&lt;p&gt;A financial audit should not feel like punishment. It is a moment of self-awareness.&lt;/p&gt;

&lt;p&gt;Set a recurring calendar reminder on the first Sunday of every month:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reconcile balances (5 mins).&lt;/li&gt;
&lt;li&gt;Review recurring subscriptions (5 mins).&lt;/li&gt;
&lt;li&gt;Inspect top category burn rates (10 mins).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Twenty minutes once a month is all it takes to gain permanent control over your financial trajectory.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Biometrics + PIN Fallback: How FinLedger Secures Keys Without a Backend Server</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Sat, 07 Nov 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/biometrics-pin-fallback-how-finledger-secures-keys-without-a-backend-server-19ik</link>
      <guid>https://dev.to/devanshu_patil/biometrics-pin-fallback-how-finledger-secures-keys-without-a-backend-server-19ik</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fojkv1uifcu82ehsjle2p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fojkv1uifcu82ehsjle2p.png" alt="Biometrics + PIN Fallback: How FinLedger Secures Keys Without a Backend Server" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When someone hands their phone to a friend to show a photo, the last thing they want is that friend casually opening their personal finance app and browsing their account balances or net worth.&lt;/p&gt;

&lt;p&gt;Mobile authentication is the front door of personal privacy.&lt;/p&gt;

&lt;p&gt;Yet many Android apps implement authentication with alarming vulnerabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Storing a plaintext 4-digit PIN in unencrypted &lt;code&gt;SharedPreferences&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Using weak, unsalted MD5 or SHA-256 hashes vulnerable to instant precomputed rainbow table attacks (a 4-digit numeric PIN has only 10,000 possible combinations, which a modern smartphone CPU can crack in less than 2 milliseconds).&lt;/li&gt;
&lt;li&gt;Using vulnerable string comparison methods (&lt;code&gt;pin == inputPin&lt;/code&gt;) susceptible to CPU execution side-channel timing attacks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is how FinLedger architected its authentication subsystem during Hardening Sprint H8, combining hardware-backed Android BiometricPrompt with 50,000-iteration salted PBKDF2 PIN hashing and constant-time verification.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With 4-Digit PINs
&lt;/h2&gt;

&lt;p&gt;A 4-digit numeric PIN has a fundamental mathematical limitation: the keyspace is tiny.&lt;/p&gt;

&lt;p&gt;From &lt;code&gt;0000&lt;/code&gt; to &lt;code&gt;9999&lt;/code&gt;, there are exactly 10,000 possible passwords. If an attacker extracts a standard SHA-256 hash of a 4-digit PIN:&lt;br&gt;
$$\text{SHA-256}("1234") = \text{03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4}$$&lt;/p&gt;

&lt;p&gt;A single CPU core can compute all 10,000 SHA-256 hashes in approximately 1.8 milliseconds. Without proper cryptographic key stretching and salting, a 4-digit hash provides virtually zero protection against an extracted database.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph WeakHashing["The Fast Hash Vulnerability"]
        direction TB
        WeakPIN["4-Digit PIN (Only 10,000 Possible Combinations)"] --&amp;gt; FastAlgo["Standard SHA-256 / MD5 Hash"]
        FastAlgo --&amp;gt; FastCrack["Cracked in under 2 milliseconds via Brute-Force!"]
    end
    subgraph HardenedPBKDF2["FinLedger PBKDF2 Key Stretching"]
        direction TB
        PINInput["User 4-Digit PIN"] --&amp;gt; Salt["16-Byte Cryptographically Secure Salt (SecureRandom)"]
        Salt --&amp;gt; PBKDF2["PBKDF2 with HMAC-SHA256:
50,000 Sequential Iterations"]
        PBKDF2 --&amp;gt; HardenedHash["Computationally Expensive Derived Hash:
Defeats GPU / ASIC Brute-Force Exhaustion"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  The Solution: Salted PBKDF2 with 50,000 Iterations
&lt;/h2&gt;

&lt;p&gt;To secure numeric PINs, FinLedger implements &lt;strong&gt;Password-Based Key Derivation Function 2 (PBKDF2)&lt;/strong&gt; combined with HMAC-SHA256:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unique 16-Byte Random Salt&lt;/strong&gt;: When the user configures their PIN, the &lt;code&gt;SecurityManager&lt;/code&gt; generates a cryptographically random 16-byte salt using &lt;code&gt;java.security.SecureRandom&lt;/code&gt;. This ensures that even if two users choose the identical PIN (&lt;code&gt;1234&lt;/code&gt;), their stored hashes are completely different, defeating precomputed rainbow tables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;50,000 Iteration Stretching&lt;/strong&gt;: PBKDF2 applies a pseudo-random function sequentially 50,000 times. This intentionally introduces a 30-millisecond computational delay for a single verification. For a legitimate user entering their PIN once, 30ms is completely imperceptible. But for an attacker attempting to brute-force all 10,000 combinations, the total attack time increases from 1.8 milliseconds to over 5 minutes of intensive computation.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Eliminating Timing Attacks with Constant-Time Comparison
&lt;/h2&gt;

&lt;p&gt;In standard Java/Kotlin, comparing two strings using &lt;code&gt;stringA == stringB&lt;/code&gt; or &lt;code&gt;stringA.equals(stringB)&lt;/code&gt; uses an early-exit comparison: it checks characters one by one and returns &lt;code&gt;false&lt;/code&gt; the microsecond it finds a mismatch.&lt;/p&gt;

&lt;p&gt;In high-security environments, an attacker measuring CPU instruction cycles can deduce which character was incorrect based on how many nanoseconds the comparison took (a &lt;strong&gt;timing side-channel attack&lt;/strong&gt;).&lt;/p&gt;

&lt;p&gt;FinLedger verifies PIN hashes using constant-time verification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MessageDigest.isEqual(storedHashBytes, computedHashBytes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;MessageDigest.isEqual&lt;/code&gt; always compares every single byte in the array regardless of where a mismatch occurs, ensuring that the execution time is strictly identical whether the first byte or the last byte differed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progressive Rate Limiting and Lockouts
&lt;/h2&gt;

&lt;p&gt;To stop automated brute-force attacks on the physical device itself, FinLedger enforces a progressive lockout state machine:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; Unlocked
    Unlocked --&amp;gt; Locked : App Backgrounded / Timeout
    Locked --&amp;gt; Unlocked : Valid Biometric or PIN
    Locked --&amp;gt; AttemptFailed : Incorrect PIN
    AttemptFailed --&amp;gt; Locked : Failed Attempts &amp;lt; 5
    AttemptFailed --&amp;gt; SoftLockout : 5 Failed Attempts (30s Lockout)
    SoftLockout --&amp;gt; HardLockout : 10 Failed Attempts (60s Lockout)
    HardLockout --&amp;gt; MaximumLockout : 15 Failed Attempts (300s Lockout)&lt;/code&gt;&lt;/pre&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5 Failed Attempts&lt;/strong&gt;: Temporary 30-second lockout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10 Failed Attempts&lt;/strong&gt;: Temporary 60-second lockout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;15 Failed Attempts&lt;/strong&gt;: Full 5-minute (300-second) lockout.&lt;/li&gt;
&lt;li&gt;Lockout counters and expiration timestamps are stored inside Android Keystore-backed &lt;code&gt;EncryptedSharedPreferences&lt;/code&gt;, ensuring that restarting the application cannot bypass active lockout countdowns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Zero-Lockout Migration Strategy
&lt;/h2&gt;

&lt;p&gt;In early development builds, FinLedger utilized a simpler single-round SHA-256 hash. When we upgraded to 50,000-iteration PBKDF2 in Sprint H8, we faced a critical deployment challenge: &lt;strong&gt;how do you upgrade existing user PINs without forcing everyone to reset their accounts?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We engineered a &lt;strong&gt;Zero-Lockout Auto-Upgrade Migration&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a user with an older hash logs in, the &lt;code&gt;SecurityManager&lt;/code&gt; verifies the entered PIN against the legacy SHA-256 algorithm.&lt;/li&gt;
&lt;li&gt;The instant authentication succeeds, the engine takes the verified plaintext PIN in memory, generates a fresh 16-byte random salt, stretches it through 50,000 iterations of PBKDF2, saves the new hardened hash, and permanently purges the legacy hash.&lt;/li&gt;
&lt;li&gt;The user experiences zero friction, and their security is transparently upgraded to modern standards on their very next login.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Android BiometricPrompt Integration
&lt;/h2&gt;

&lt;p&gt;Alongside the hardened PIN, FinLedger integrates directly with the AndroidX &lt;code&gt;BiometricPrompt&lt;/code&gt; API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Allows instant authentication using fingerprint sensors or secure 3D face recognition.&lt;/li&gt;
&lt;li&gt;Uses &lt;code&gt;BIOMETRIC_STRONG&lt;/code&gt; crypto-based authentication backed by the hardware TEE (Trusted Execution Environment).&lt;/li&gt;
&lt;li&gt;If biometric authentication fails or the sensor is dirty, the user seamlessly falls back to the PBKDF2 PIN keypad.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;A 4-digit PIN can be simple for the user without being easy for an attacker.&lt;/p&gt;

&lt;p&gt;By combining hardware-backed biometrics with 50,000 iterations of salted PBKDF2, constant-time verification, and progressive lockout state machines, FinLedger delivers consumer convenience backed by defense-grade security.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>What Happens When You Delete a Bank Account With 500 Transactions Behind It?</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Fri, 06 Nov 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/what-happens-when-you-delete-a-bank-account-with-500-transactions-behind-it-27de</link>
      <guid>https://dev.to/devanshu_patil/what-happens-when-you-delete-a-bank-account-with-500-transactions-behind-it-27de</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlshhkrpt3odal17ifnm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjlshhkrpt3odal17ifnm.png" alt="What Happens When You Delete a Bank Account With 500 Transactions Behind It?" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a personal finance app, deleting an individual transaction is a minor recalculation.&lt;/p&gt;

&lt;p&gt;Deleting an entire &lt;strong&gt;account&lt;/strong&gt;, however, is the financial equivalent of detonating a structural support beam inside a building.&lt;/p&gt;

&lt;p&gt;Consider what an account actually represents:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It holds an opening balance and a running balance.&lt;/li&gt;
&lt;li&gt;It is referenced by hundreds or thousands of historical transactions spanning multiple years.&lt;/li&gt;
&lt;li&gt;It may have acted as the source or destination for dozens of inter-account transfers to other bank accounts.&lt;/li&gt;
&lt;li&gt;It may have linked active investment holdings, recurring bill schedules, or credit card billing statements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your application allows a user to delete an account with a single unverified &lt;code&gt;DELETE FROM accounts WHERE id = ?&lt;/code&gt;, your database will either crash due to SQLite foreign key constraint failures or leave behind severed counter-party balances.&lt;/p&gt;

&lt;p&gt;Here is how FinLedger designs account deletion safety through a multi-step defense-in-depth pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Archival Alternative: Why You Rarely Want to Delete
&lt;/h2&gt;

&lt;p&gt;Before discussing deletion, the most important product decision is offering &lt;strong&gt;Account Archival&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In 90% of real-world scenarios, a user does not want to destroy an account's history; they simply closed a bank account or canceled a credit card:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;They switched from Bank A to Bank B.&lt;/li&gt;
&lt;li&gt;They closed an old credit card to avoid an annual fee.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph ArchivalVsDeletion["Archival vs Deletion"]
        direction TB
        UserAction["User Closed a Bank Account"]

        subgraph SafeArchival["Option A: Archive Account (Recommended)"]
            A1["Set isArchived = true"]
            A2["Hide from Daily Account Pickers &amp;amp; Transfer Dropdowns"]
            A3["Preserve 100% Historical Statements &amp;amp; Past Tax Reports"]
            A4["Retain Relational Foreign Key Integrity Forever"]
        end

        subgraph PermanentDeletion["Option B: Permanent Deletion (Danger Zone)"]
            D1["Double Confirmation Dialog"]
            D2["Verify No Active Dependencies (Loans, Portfolios)"]
            D3["Recalculate All Counter-Party Accounts from Past Transfers"]
            D4["Cascade Delete Transactions, Attachments, and Statements"]
        end

        UserAction --&amp;gt; SafeArchival
        UserAction --&amp;gt; PermanentDeletion
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;When an account is archived:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;isArchived&lt;/code&gt; flag is set to &lt;code&gt;true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The account is immediately hidden from active transaction entry pickers, transfer dropdowns, and daily dashboard cards.&lt;/li&gt;
&lt;li&gt;Crucially, all historical transactions, past annual tax statements, and carried-forward ledger mathematics remain 100% intact.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Permanent Account Deletion: The Safety Pipeline
&lt;/h2&gt;

&lt;p&gt;When a user insists on permanent deletion (located inside the Settings "Danger Zone"), FinLedger executes a strict four-stage safety sequence:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph DeletionStages["Account Deletion Safety Pipeline"]
        direction TB
        Stage1["1. Dependency Inspection:
Check for Active Portfolio Holdings or Linked Loans"] --&amp;gt; Stage2["2. Double-Confirmation Guard:
Explicit Warning Dialog Requiring Confirmation"]
        Stage2 --&amp;gt; Stage3["3. Counter-Party Recalculation:
Refund all external accounts involved in past transfers"]
        Stage3 --&amp;gt; Stage4["4. Atomic Cascade Cleanup:
Purge transactions, disk attachments, and statements"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Stage 1: Dependency Check (&lt;code&gt;BlockedByDependencies&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Before allowing deletion, &lt;code&gt;AccountRepository.deleteAccount()&lt;/code&gt; inspects relational dependencies:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does this account have active portfolio investment holdings?&lt;/li&gt;
&lt;li&gt;Is this account linked to outstanding loan repayments?
If active dependent records exist, the operation is blocked: &lt;code&gt;AccountDeletionResult.BlockedByDependencies&lt;/code&gt;. The user is instructed to close or transfer their active holdings first.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 2: The Double-Confirmation Guard
&lt;/h3&gt;

&lt;p&gt;Account deletion is quarantined behind a multi-step confirmation dialog:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The dialog explicitly lists the consequences: the exact number of transactions that will be purged and the total balance that will be eliminated.&lt;/li&gt;
&lt;li&gt;The user must explicitly confirm the action.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 3: Counter-Party Recalculation
&lt;/h3&gt;

&lt;p&gt;What happens to past transfers?&lt;br&gt;
Suppose Account A transferred 10,000 rupees to Account B four months ago. If Account A is deleted, what happens to Account B?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you simply delete the transfer record, Account B's balance will become invalid upon the next historical ledger recalculation.&lt;/li&gt;
&lt;li&gt;FinLedger's deletion engine iterates through all past transfer records involving the deleted account, recalculates the running balances of all counter-party accounts atomically, and posts compensating ledger adjustments where required.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Stage 4: Resolving SQLite Foreign Key Constraints (Commit &lt;code&gt;fd19ad0&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;In early versions, deleting an account that had linked credit card statements or vault records could trigger an immediate SQLite &lt;code&gt;FOREIGN KEY constraint failed&lt;/code&gt; exception.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In commit &lt;code&gt;fd19ad0&lt;/code&gt;, we audited the exact cascade deletion order inside &lt;code&gt;AppDatabase.withTransaction&lt;/code&gt;:

&lt;ol&gt;
&lt;li&gt;Unlink and delete child statement records.&lt;/li&gt;
&lt;li&gt;Prune linked receipt photo attachments from device disk.&lt;/li&gt;
&lt;li&gt;Delete child transaction entities.&lt;/li&gt;
&lt;li&gt;Finally, remove the parent account entity.&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preserving the Single Primary Account Invariant
&lt;/h2&gt;

&lt;p&gt;FinLedger enforces a strict invariant: &lt;strong&gt;there must always be exactly one primary account&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a user deletes an account that was currently marked as the primary account (&lt;code&gt;isPrimary = true&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The repository's atomic transaction automatically selects the next available liquid account (such as the default Cash Wallet) and elevates it to primary (&lt;code&gt;clearPrimaryAccounts()&lt;/code&gt; followed by &lt;code&gt;setPrimaryAccount()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The application is never left in an invalid state where zero primary accounts exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Deleting financial accounts requires comprehensive architectural defensive programming.&lt;/p&gt;

&lt;p&gt;By prioritizing non-destructive archival, inspecting dependencies before execution, recomputing counter-party transfer balances, and respecting SQLite foreign key cascade order, FinLedger guarantees that even destructive actions never corrupt your ledger.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>How I Store Merchant Suggestions Without Polluting Relational Database Keys</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Fri, 06 Nov 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/how-i-store-merchant-suggestions-without-polluting-relational-database-keys-1cin</link>
      <guid>https://dev.to/devanshu_patil/how-i-store-merchant-suggestions-without-polluting-relational-database-keys-1cin</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpomf2.lain.la%2Ff%2Fcff7fbng.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpomf2.lain.la%2Ff%2Fcff7fbng.png" alt="How I Store Merchant Suggestions Without Polluting Relational Database Keys"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When designing a database for an expense tracker, normalization is usually the first instinct of any software engineer.&lt;/p&gt;

&lt;p&gt;You create a &lt;code&gt;transactions&lt;/code&gt; table. A transaction has a merchant. So you immediately create a normalized &lt;code&gt;merchants&lt;/code&gt; table with an auto-incrementing primary key (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;category_id&lt;/code&gt;), and add a foreign key column to your transaction table: &lt;code&gt;merchant_id REFERENCES merchants(id)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It feels like textbook database design.&lt;/p&gt;

&lt;p&gt;Then real users start typing real-world merchant names into your application, and that relational foreign key constraint quickly turns into an engineering nightmare.&lt;/p&gt;

&lt;p&gt;Here is why FinLedger deliberately decoupled merchant auto-suggestions from relational foreign keys, and how we built an on-device merchant intelligence engine without database pollution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Normalization Trap: Why Foreign Keys Fail for Merchants
&lt;/h2&gt;

&lt;p&gt;In the real world, merchant names are noisy, erratic, and constantly shifting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A user buys coffee at &lt;em&gt;"Blue Tokai Coffee Roasters"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Next week, their credit card SMS alerts them about a transaction at &lt;em&gt;"BLUE TOKAI COFFEE"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Two weeks later, they manually enter an expense as &lt;em&gt;"Blue Tokai"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Three weeks later, they visit a branch at the airport and type &lt;em&gt;"Blue Tokai - Terminal 2"&lt;/em&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph StrictRelational["The Strict Relational Anti-Pattern"]
        direction TB
        M1["merchants Table:
id=1: 'Blue Tokai'
id=2: 'Blue Tokai Coffee'
id=3: 'BLUE TOKAI COFFEE ROASTERS'"]
        Tx["transactions Table:
Foreign Key: merchant_id -&amp;gt; Rigid Dependency!"]
        M1 --- Tx
        Problem["What happens when the user cleans up duplicate merchants?
Deleting or merging merchant rows requires cascading updates across 
thousands of historical transaction records!"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If your transaction table enforces a strict foreign key constraint (&lt;code&gt;merchant_id&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Proliferation of Duplicates&lt;/strong&gt;: Every minor spelling variation, capitalization difference, or branch suffix forces the creation of a brand new row in the &lt;code&gt;merchants&lt;/code&gt; table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cascading Migration Nightmares&lt;/strong&gt;: If a user wants to merge or rename a merchant, SQLite must rewrite foreign key pointers across hundreds of historical transactions. If an account is deleted or transactions are purged, foreign key constraints risk breaking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SMS Ingestion Friction&lt;/strong&gt;: Automated SMS parsers often extract truncated merchant strings (e.g. &lt;em&gt;"VSI*UBER INDIA"&lt;/em&gt;). Forcing an automated parser to match or create a permanent relational merchant entity before saving a transaction creates unnecessary pipeline complexity.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The FinLedger Architecture: Flexible Text + Independent Learning
&lt;/h2&gt;

&lt;p&gt;To solve this, FinLedger enforces a clean separation of concerns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transactions store merchants as pure, decoupled text attributes (&lt;code&gt;purposeMerchant: String&lt;/code&gt;).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Meanwhile, merchant auto-suggestions, category predictions, and usage frequency are managed by an entirely independent subsystem: the &lt;strong&gt;Merchant Rule &amp;amp; Intelligence Engine&lt;/strong&gt; (&lt;code&gt;MerchantRuleEntity&lt;/code&gt;, &lt;code&gt;MerchantRuleDao&lt;/code&gt;, and &lt;code&gt;LocalCategoryIntelligenceEngine&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph DecoupledArchitecture["FinLedger Decoupled Merchant Architecture"]
        direction TB
        TxEntity["Transaction Entity:
amount: 450.00
purposeMerchant: 'Blue Tokai' (Plain String)
categoryId: 3 (Food &amp;amp; Dining)"]

        RuleEntity["MerchantRule Entity (Independent):
normalizedPattern: 'blue tokai'
preferredCategoryId: 3
usageCount: 14
lastUsedTimestamp: 1729864200000"]

        TxEntity -.-&amp;gt;|"Learns From (Asynchronous)"| RuleEntity
        RuleEntity -.-&amp;gt;|"Provides Auto-Suggestions To"| UI["Add Transaction UI Screen"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. The Transaction Entity Remains Resilient
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;purposeMerchant&lt;/code&gt; is a standard string column, transactions have zero foreign key dependencies on merchant entities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A transaction can be edited, copied, imported from a bank statement, or deleted without worrying about orphan relational keys.&lt;/li&gt;
&lt;li&gt;Truncated SMS names or detailed manual notes exist safely side-by-side.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The Auto-Learning Rule Engine
&lt;/h3&gt;

&lt;p&gt;Whenever a transaction is confirmed in &lt;code&gt;TransactionRepository&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The engine extracts the merchant string and normalizes it (lowercase, trimmed whitespace, special characters removed).&lt;/li&gt;
&lt;li&gt;It queries the &lt;code&gt;merchant_rules&lt;/code&gt; table. If the normalized pattern exists, it increments &lt;code&gt;usageCount&lt;/code&gt;, updates &lt;code&gt;lastUsedTimestamp&lt;/code&gt;, and refines the associated &lt;code&gt;preferredCategoryId&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the pattern is new, it inserts a new rule learning that this merchant belongs to this category.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Intelligent Auto-Suggestions with Blank Query Support
&lt;/h2&gt;

&lt;p&gt;When a user taps into the merchant field in &lt;code&gt;ModernAddTransactionScreen&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;As they type &lt;em&gt;"bl"&lt;/em&gt;, the repository queries &lt;code&gt;MerchantRuleDao.searchMerchantRules("bl%")&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Results are ranked dynamically by &lt;strong&gt;recency and frequency&lt;/strong&gt; (frequently visited merchants appear at the top of the dropdown).&lt;/li&gt;
&lt;li&gt;Tapping a suggestion auto-fills the merchant name &lt;strong&gt;and&lt;/strong&gt; automatically selects the associated spending category with a single tap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fixing the Blank Query Edge Case (Commit &lt;code&gt;8922c14&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;A subtle UX defect in early versions occurred when a user opened the merchant field without typing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the search query was blank, the dropdown showed nothing.&lt;/li&gt;
&lt;li&gt;In commit &lt;code&gt;8922c14&lt;/code&gt;, we updated the query pipeline: when the search query is blank, the engine automatically surfaces the user's top 5 most frequently used recent merchants. If a user clears their search query, the suggestion sheet gracefully re-opens rather than closing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Database normalization is a tool, not a religious dogma.&lt;/p&gt;

&lt;p&gt;Keeping merchant names as decoupled text attributes in your transaction tables while managing auto-complete intelligence in an independent learning engine gives you the best of both worlds: frictionless data entry for users and zero relational fragility in your database.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Layers, Boundaries, and Leaks: Clean Android Architecture for Financial Apps</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/layers-boundaries-and-leaks-clean-android-architecture-for-financial-apps-1gk1</link>
      <guid>https://dev.to/devanshu_patil/layers-boundaries-and-leaks-clean-android-architecture-for-financial-apps-1gk1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujve5vzhyhiy3jc82fjh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fujve5vzhyhiy3jc82fjh.png" alt="Layers, Boundaries, and Leaks: Clean Android Architecture for Financial Apps" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Architecture diagrams always look beautiful in blog posts.&lt;/p&gt;

&lt;p&gt;You draw clean concentric circles: the UI layer points to the Presentation layer, which points to Domain UseCases, which points to Repositories, which points to the Database. Everything looks orderly, modular, and decoupled.&lt;/p&gt;

&lt;p&gt;Then you write 100,000 lines of production code over several months.&lt;/p&gt;

&lt;p&gt;Features are added rapidly; release deadlines loom; edge cases arise. Slowly, boundaries begin to erode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A ViewModel starts executing raw SQL queries to save five minutes.&lt;/li&gt;
&lt;li&gt;A Repository starts formatting user-facing date strings.&lt;/li&gt;
&lt;li&gt;A composable screen calls three different repositories directly because creating an aggregator feels like boilerplate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Recently, we completed an exhaustive, 360-file architectural audit of FinLedger (documented in &lt;code&gt;FINLEDGER_FULL_APPLICATION_AUDIT.md&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;Here is a transparent look at the anatomy of FinLedger's architecture: what worked brilliantly, where architectural erosion crept in, and how we are refactoring our largest classes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architectural Blueprint
&lt;/h2&gt;

&lt;p&gt;FinLedger is organized according to Clean Architecture and MVI/MVVM presentation patterns:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph UI_Layer["1. Presentation Layer (Jetpack Compose)"]
        direction TB
        Screens["Screens &amp;amp; Composables:
LedgerScreen, DashboardScreen, CreditCardDetailScreen"]
        Tokens["Design System Tokens (Colors, Shapes, Elevation)"]
    end
    subgraph VM_Layer["2. ViewModel Layer (StateFlow &amp;amp; Coroutines)"]
        VMs["ViewModels:
LedgerViewModel, BudgetViewModel, VaultViewModel"]
        Pipelines["Reactive Pipelines &amp;amp; UDF State Bundles"]
    end
    subgraph Domain_Layer["3. Pure Domain Layer (Zero Android Dependencies)"]
        Engines["Domain Engines:
LedgerEngine, Posting.kt, CreditCardBillingEngine"]
        Models["Immutable Domain Models &amp;amp; Validation Rules"]
    end
    subgraph Data_Layer["4. Data Layer (Persistence &amp;amp; Security)"]
        Repos["Specialized Repositories:
TransactionRepo, AccountRepo, LendingRepo, VaultRepo"]
        DB[("Room + SQLCipher Encrypted SQLite
(33 Entities, 24 DAOs, Migrations 1 -&amp;gt; 24)")]
        Security["KeyStore &amp;amp; SecurityManager (PBKDF2)"]
    end

    UI_Layer --&amp;gt;|"User Intents"| VM_Layer
    VM_Layer --&amp;gt;|"Immutable StateFlow"| UI_Layer
    VM_Layer --&amp;gt;|"Domain Commands"| Repos
    Repos --&amp;gt;|"Delegates Calculations"| Domain_Layer
    Repos &amp;lt;--&amp;gt;|"withTransaction ACID"| DB&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  The Triumphs of This Design
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Isolated Accounting Math&lt;/strong&gt;: Consolidating all posting rules into &lt;code&gt;Posting.kt&lt;/code&gt; and &lt;code&gt;LedgerEngine.kt&lt;/code&gt; completely eliminated balance calculation drift between UI screens and background workers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lightning-Fast JVM Testing&lt;/strong&gt;: Because the domain engine contains zero &lt;code&gt;android.*&lt;/code&gt; imports, all 1,570 automated unit tests execute directly on the JVM in under 12 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database Security Isolation&lt;/strong&gt;: SQLCipher page-level encryption and KeyStore master key management remain completely encapsulated in the data layer; UI screens have zero awareness of cryptographic ciphers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where Architecture Eroded: The "God Object" Problem
&lt;/h2&gt;

&lt;p&gt;When an application grows to 100,324 lines of Kotlin code across 360 source files, file concentration is the most insidious form of architectural erosion.&lt;/p&gt;

&lt;p&gt;Our October audit uncovered a stark reality: &lt;strong&gt;the top 10 largest files accounted for over 17,900 lines of code—nearly 18% of the entire codebase!&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Monolithic File&lt;/th&gt;
&lt;th&gt;Line Count&lt;/th&gt;
&lt;th&gt;Primary Responsibility&lt;/th&gt;
&lt;th&gt;Architectural Flaw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BackupRestoreManager.kt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3,043 LOC&lt;/td&gt;
&lt;td&gt;Encrypted backup/restore&lt;/td&gt;
&lt;td&gt;Acts as codec, disk unpacker, and cloud sync coordinator simultaneously.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VaultDialogs.kt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,813 LOC&lt;/td&gt;
&lt;td&gt;Secret Fund UI modals&lt;/td&gt;
&lt;td&gt;Houses 14 distinct setup dialogs and location sliders in a single file.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ModernAddTransactionScreen.kt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,985 LOC&lt;/td&gt;
&lt;td&gt;Transaction entry UI&lt;/td&gt;
&lt;td&gt;Mixes layout composables with complex input validation and category auto-selection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DatabaseReconciliationEngine.kt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,502 LOC&lt;/td&gt;
&lt;td&gt;Whole-app ledger recalculation&lt;/td&gt;
&lt;td&gt;Contains both audit algorithms and direct database update routines.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SplitGroupDetailScreen.kt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,497 LOC&lt;/td&gt;
&lt;td&gt;Group expenses UI&lt;/td&gt;
&lt;td&gt;Combines 3 distinct tab layouts (Bills, Balances, History) in one file.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why does this happen?&lt;br&gt;
It happens because &lt;strong&gt;convenience compounds&lt;/strong&gt;. When adding a new backup feature, it is always faster to add a private function to an existing &lt;code&gt;BackupRestoreManager.kt&lt;/code&gt; than to design a new modular driver interface. Over months, a 400-line class quietly becomes a 3,000-line monolith.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Storage vs. Database ACID Boundary Window
&lt;/h2&gt;

&lt;p&gt;The audit also exposed a subtle boundary leak between SQLite and the Android filesystem.&lt;/p&gt;

&lt;p&gt;When a user deletes a transaction with receipt photos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The database rows in the &lt;code&gt;transactions&lt;/code&gt; and &lt;code&gt;transaction_attachments&lt;/code&gt; tables delete within an atomic Room transaction (&lt;code&gt;withTransaction&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;But deleting the physical JPEG files from disk (&lt;code&gt;File.delete()&lt;/code&gt;) must happen outside SQLite, because the local filesystem is not managed by SQLite's transaction journal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an unexpected process termination or power loss occurs in the millisecond window between the database commit and the disk file deletion, an orphaned image file remains in private app storage.&lt;/p&gt;

&lt;p&gt;Identifying this leak allowed us to implement an automated startup hygiene sweep in &lt;code&gt;AttachmentRepository&lt;/code&gt; to quietly prune orphaned disk files.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Modular Refactoring Roadmap for v2.1.2
&lt;/h2&gt;

&lt;p&gt;A real engineering architecture is not a static monument; it is a living system that requires continuous maintenance.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph Monolith["BackupRestoreManager (3,043 LOC)"]
        direction TB
        CodeAll["Everything in one file"]
    end
    subgraph Modular["v2.1.2 Modular Decomposition"]
        direction TB
        Codec["BackupPayloadCodec:
Pure JSON Serialization &amp;amp; SHA-256 Envelope"]
        Cipher["BackupVaultCipher:
AES-256-GCM Cryptographic Engine"]
        Runner["LocalRestoreRunner:
Atomic Filesystem Unpacking &amp;amp; Rollback"]
        Driver["CloudSyncDriver:
Google Drive REST v3 Transport"]
    end
    Monolith ==&amp;gt;|"Refactored Into"| Modular&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Based on our architectural audit, our refactoring plan decomposes our largest classes into single-responsibility drivers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Splitting &lt;code&gt;BackupRestoreManager.kt&lt;/code&gt; into &lt;code&gt;BackupPayloadCodec&lt;/code&gt; (pure serialization), &lt;code&gt;BackupVaultCipher&lt;/code&gt; (cryptography), &lt;code&gt;LocalRestoreRunner&lt;/code&gt; (filesystem rollback), and &lt;code&gt;GoogleDriveSyncDriver&lt;/code&gt; (network transport).&lt;/li&gt;
&lt;li&gt;Decomposing &lt;code&gt;VaultDialogs.kt&lt;/code&gt; into 6 modular dialog composables.&lt;/li&gt;
&lt;li&gt;Decoupling &lt;code&gt;ModernAddTransactionScreen.kt&lt;/code&gt; into presentation-only widgets and a standalone &lt;code&gt;TransactionInputValidator&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Clean Architecture is not about achieving ideological purity; it is about managing cognitive complexity and change resistance.&lt;/p&gt;

&lt;p&gt;When your codebase grows, audit your boundaries regularly. Acknowledge your monoliths honestly, and refactor before file concentration turns your architecture into legacy code.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Building a Finance Filter Sheet That Doesn't Require a PhD to Use</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Thu, 05 Nov 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/building-a-finance-filter-sheet-that-doesnt-require-a-phd-to-use-4129</link>
      <guid>https://dev.to/devanshu_patil/building-a-finance-filter-sheet-that-doesnt-require-a-phd-to-use-4129</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcmb33cfu73vkd4s33tei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcmb33cfu73vkd4s33tei.png" alt="Building a Finance Filter Sheet That Doesn't Require a PhD to Use" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a personal finance ledger grows from fifty transactions to five thousand, finding specific financial events becomes a needle-in-a-haystack problem.&lt;/p&gt;

&lt;p&gt;A simple search bar can match a merchant name like &lt;em&gt;"Amazon"&lt;/em&gt;, but text search alone cannot answer multidimensional financial questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;"Show me all credit card purchases between 1,000 and 5,000 rupees during our trip to Goa last August."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Show me all cash expenses categorized under Dining across the last three months."&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;"Show me all transactions that have receipt photo attachments uploaded."&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Answering these questions requires &lt;strong&gt;multi-dimensional filtering&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how FinLedger designed and built its multi-dimensional filter bottom sheet (&lt;code&gt;LedgerFilterSheet.kt&lt;/code&gt;) in Jetpack Compose, supporting real-time multi-variable constraints without lag.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Five Dimensions of a Financial Filter
&lt;/h2&gt;

&lt;p&gt;In Sprint 8 (&lt;code&gt;commit 43d9bfb&lt;/code&gt;), we formalized the multidimensional filter model into five orthogonal filter axes:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;classDiagram
    class LedgerFilterState {
        +DateRangeFilter dateRange
        +Set~Long~ selectedAccountIds
        +Set~TransactionType~ selectedTypes
        +Set~Long~ selectedCategoryIds
        +ClosedFloatingPointRange amountRange
        +Boolean hasAttachmentsOnly
        +Boolean isDefault()
        +Int activeFilterCount()
    }
    class DateRangeFilter {
        ALL_TIME
        THIS_MONTH
        LAST_MONTH
        CUSTOM_RANGE(start, end)
    }
    LedgerFilterState *-- DateRangeFilter&lt;/code&gt;&lt;/pre&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Temporal Dimension (Date Range)&lt;/strong&gt;: All Time, This Month, Last Month, or a custom date range picker (&lt;code&gt;startDate&lt;/code&gt; to &lt;code&gt;endDate&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account Dimension&lt;/strong&gt;: Filter to any combination of specific accounts (e.g. HDFC Checking + Cash Wallet, excluding Credit Cards).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transaction Type Dimension&lt;/strong&gt;: Income, Expense, Transfer, Credit Card Purchase, or Card Repayment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Categorical Dimension&lt;/strong&gt;: Filter across multiple spending categories (e.g. Groceries + Dining Out + Fuel).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Magnitude Dimension (Amount Slider)&lt;/strong&gt;: A dual-thumb range slider filtering transactions between a minimum and maximum monetary amount (e.g. 500 to 10,000 rupees).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attachment Presence&lt;/strong&gt;: A quick toggle filtering transactions that have attached receipt photos.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Jetpack Compose Architecture: Bottom Sheet State Hoisting
&lt;/h2&gt;

&lt;p&gt;To provide a smooth user experience, the filter sheet is rendered as a modal bottom sheet using Material 3 &lt;code&gt;ModalBottomSheet&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A critical architectural consideration in Jetpack Compose is &lt;strong&gt;state hoisting&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph StateHoistingFlow["Filter State Hoisting Flow"]
        direction LR
        VM["LedgerViewModel:
Holds Authoritative _filterState"] --&amp;gt; Sheet["LedgerFilterSheet (Composable):
Maintains Local Draft State"]
        Sheet --&amp;gt;|"User Toggles Chips / Adjusts Sliders"| Draft["Local Draft Updates Fluidly"]
        Draft --&amp;gt;|"User Taps 'Apply Filters'"| Apply["Emit New State to ViewModel"]
        Draft --&amp;gt;|"User Taps 'Reset All'"| Reset["Revert Local Draft to Default"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. Isolated Draft State
&lt;/h3&gt;

&lt;p&gt;If the bottom sheet modifies the ViewModel's active filter state on every single chip tap or slider drag, the underlying transaction list will continuously re-filter and recompose in the background while the user is still making decisions.&lt;/p&gt;

&lt;p&gt;Instead, &lt;code&gt;LedgerFilterSheet&lt;/code&gt; creates a local copy (&lt;code&gt;rememberSaveable&lt;/code&gt;) of the filter state upon opening. Users can toggle multiple categories, adjust price sliders, and change date ranges with instant local UI responsiveness.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Explicit Commit and Reset
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Apply Filters Button&lt;/strong&gt;: Commits the local draft state back to &lt;code&gt;LedgerViewModel._filterState&lt;/code&gt;. The bottom sheet dismisses, and the ledger updates in a single unified emission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reset All Button&lt;/strong&gt;: Resets all five dimensions back to default in one tap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Active Filter Badge&lt;/strong&gt;: The filter icon on the ledger top bar displays a numbered badge indicating exactly how many filter dimensions are currently active (e.g. &lt;em&gt;"3"&lt;/em&gt; if filtering by Category, Account, and Date Range).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-Time Slider Performance with Paisa Snapping
&lt;/h2&gt;

&lt;p&gt;One of the most complex UI components in the sheet is the dual-thumb amount range slider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It must handle ranges from 0 rupees to 500,000+ rupees.&lt;/li&gt;
&lt;li&gt;If the slider steps are linear, adjusting small amounts (like 250 rupees) on a 500,000-rupee scale is nearly impossible because 1 millimeter of finger movement jumps by 10,000 rupees.&lt;/li&gt;
&lt;li&gt;FinLedger solves this using a &lt;strong&gt;logarithmic scale curve&lt;/strong&gt; for the slider values: fine-grained 50-rupee steps at lower amounts (under 5,000), expanding to 500-rupee steps at medium amounts, and 5,000-rupee steps at high amounts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Zero Distortions in Filtered Exports
&lt;/h2&gt;

&lt;p&gt;A major advantage of formalizing &lt;code&gt;LedgerFilterState&lt;/code&gt; is seamless export integration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a user applies a filter (e.g. &lt;em&gt;"All Medical expenses in 2026"&lt;/em&gt;), and then taps &lt;strong&gt;Export CSV&lt;/strong&gt; or &lt;strong&gt;Generate PDF Statement&lt;/strong&gt;, the export engines read the exact same filtered transaction set.&lt;/li&gt;
&lt;li&gt;The exported vector PDF statement displays the active filter criteria directly in its header, providing an authoritative, filtered financial document ready for tax filing or expense reimbursement.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;A financial ledger is only as powerful as its query capabilities.&lt;/p&gt;

&lt;p&gt;By organizing filters into orthogonal dimensions, isolating local draft state inside a modal bottom sheet, and integrating filtered sets directly into PDF and CSV export pipelines, FinLedger turns years of complex transaction history into an instantly searchable, audit-ready database.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>The Search Bug That Silently Corrupted My Running Balance Display</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Wed, 04 Nov 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/the-search-bug-that-silently-corrupted-my-running-balance-display-2im3</link>
      <guid>https://dev.to/devanshu_patil/the-search-bug-that-silently-corrupted-my-running-balance-display-2im3</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxxvoxj21z40adndsw2n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkxxvoxj21z40adndsw2n.png" alt="The Search Bug That Silently Corrupted My Running Balance Display" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Search is one of the most frequently used features in any financial application.&lt;/p&gt;

&lt;p&gt;A user opens their transaction ledger, taps the search bar, and types &lt;em&gt;"Starbucks"&lt;/em&gt; or &lt;em&gt;"Electric Utility"&lt;/em&gt;. They want to know two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When did these transactions take place?&lt;/li&gt;
&lt;li&gt;Exactly how much money did I spend on this specific merchant or utility?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the user interface is poorly architected, however, searching a ledger creates bizarre, misleading financial summaries.&lt;/p&gt;

&lt;p&gt;Here is why FinLedger's search pipeline dynamically decouples Carried-Forward opening balances from search results, ensuring that search headers provide immediate, actionable answers without financial distortion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cognitive Trap of Static Ledger Headers
&lt;/h2&gt;

&lt;p&gt;Consider a user who has an active checking account with an ending balance of &lt;strong&gt;125,000 rupees&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They open their ledger feed and type &lt;em&gt;"Gym"&lt;/em&gt; into the search box. The list immediately filters down to show two matching transactions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;October 1: Annual Gym Membership — &lt;strong&gt;12,000 rupees&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;June 15: Protein Shake at Gym Cafe — &lt;strong&gt;350 rupees&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph ConfusingUI["The Confusing Static Header Anti-Pattern"]
        direction TB
        Search["Search Active: 'Gym' (2 matches)"]
        Header["Header Summary Card:
Account Balance: 125,000
Carried-Forward (C/F): 137,350
Total Spend: 12,350"]
        UserConfusion["User Confusion:
'Why does the header say 125,000 when I searched Gym?
What does the 137,350 Carried-Forward number mean here?'"]
        Search --&amp;gt; Header --&amp;gt; UserConfusion
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If the summary top card continues displaying the total account balance (125,000) and the full Carried-Forward opening equity (137,350), what does that header communicate?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user is looking at a filtered view of two gym transactions, but the top card is blaring a 125,000-rupee balance.&lt;/li&gt;
&lt;li&gt;If the header shows "Net Flow = -12,350", but adds that to the total account opening balance, the numbers on screen completely contradict the filtered rows below them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user's brain has to mentally filter out the header because it represents the whole account, while the list represents the search query.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Dynamic Search Mode Transitions
&lt;/h2&gt;

&lt;p&gt;In FinLedger, entering a search query triggers a formal &lt;strong&gt;contextual transition&lt;/strong&gt; in the &lt;code&gt;LedgerViewModel&lt;/code&gt; data pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph SearchStateSwitch["Ledger Header Contextual Switch"]
        direction LR
        Normal["Normal Feed Mode:
1. Display Carried-Forward Starting Balance
2. Display Period Income &amp;amp; Expense
3. Display Authoritative Ending Balance"]

        SearchActive["Search Mode Active:
1. Zero Out Carried-Forward Balance (C/F = 0.00)
2. Display Filtered Search Total Sum
3. Display Count of Matched Records"]

        Normal &amp;lt;--&amp;gt;|"User Types in Search Box (Debounced 200ms)"| SearchActive
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. Zeroing Out the Carried-Forward Balance
&lt;/h3&gt;

&lt;p&gt;When a search query is active (e.g. &lt;code&gt;query.isNotBlank()&lt;/code&gt;), the concept of a historical opening balance is irrelevant. The user is not auditing their bank statement; they are aggregating a subset of historical expenses.&lt;/p&gt;

&lt;p&gt;The engine explicitly sets:&lt;br&gt;
$$\text{Carried-Forward (C/F)} = 0.00$$&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Displaying the Matched Search Total
&lt;/h3&gt;

&lt;p&gt;The top summary card dynamically transforms into a &lt;strong&gt;Search Result Breakdown&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matched Total&lt;/strong&gt;: Displays the exact sum of all matching expenses ($\sum \text{matched amounts}$). In the gym example, it clearly reads: &lt;em&gt;"2 transactions found: -12,350.00"&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;If the search returns both income and expenses (such as searching a contact's name who both borrowed and repaid money), the header shows:
$$\text{Net Flow} = \sum \text{Matched Income} - \sum \text{Matched Expenses}$$&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Instant Restoral on Dismissal
&lt;/h3&gt;

&lt;p&gt;The moment the user taps the clear icon or backtracks out of the search bar, the pipeline smoothly transitions back to the authoritative ledger mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The true Carried-Forward starting balance is restored.&lt;/li&gt;
&lt;li&gt;The complete date-grouped transaction feed recomposes in milliseconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance: Debounced Asynchronous Filtering
&lt;/h2&gt;

&lt;p&gt;In large personal ledgers spanning years of records, searching through thousands of merchant strings, notes, and tags can strain the CPU if executed naively on every keystroke.&lt;/p&gt;

&lt;p&gt;FinLedger optimizes this using Kotlin Coroutine operators:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Debounce (200ms)&lt;/strong&gt;: The search input stream is throttled using &lt;code&gt;_searchQuery.debounce(200)&lt;/code&gt;. Rapid typing does not trigger repetitive filtering until the user's fingers pause.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background Execution (&lt;code&gt;Dispatchers.Default&lt;/code&gt;)&lt;/strong&gt;: String matching, case-insensitive normalization, and amount aggregation execute entirely on background worker threads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date Preservation&lt;/strong&gt;: Matched search results retain their sticky date headers so the user can see exactly when each transaction occurred across past years.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Good software adapts its presentation to match user intent.&lt;/p&gt;

&lt;p&gt;When a user searches their ledger, they want to aggregate a specific category of spending, not inspect their total bank balance. Decoupling carried-forward totals during search provides instant clarity while protecting the mathematical integrity of the feed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Why `withTransaction` Is the Only Safe Way to Post Financial Entries in SQLite</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Wed, 04 Nov 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/why-withtransaction-is-the-only-safe-way-to-post-financial-entries-in-sqlite-3dom</link>
      <guid>https://dev.to/devanshu_patil/why-withtransaction-is-the-only-safe-way-to-post-financial-entries-in-sqlite-3dom</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80huuxz9ldgg0pei1vd2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F80huuxz9ldgg0pei1vd2.png" alt="Why  raw `withTransaction` endraw  Is the Only Safe Way to Post Financial Entries in SQLite" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mobile applications execute in a chaotic, hostile runtime environment.&lt;/p&gt;

&lt;p&gt;Unlike backend servers running on uninterrupted data center power, a mobile app can be terminated by the Android operating system at virtually any microsecond:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user can swipe the app away in the Android task switcher.&lt;/li&gt;
&lt;li&gt;The device battery can run completely out of charge.&lt;/li&gt;
&lt;li&gt;The Android low-memory killer (LMK) can terminate background processes to prioritize an incoming phone call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your financial application updates multiple database tables without strict transactional atomicity, a poorly timed process death will split your database in half: some rows will be written, others will be lost, and your accounts will be permanently out of balance.&lt;/p&gt;

&lt;p&gt;Here is how FinLedger guarantees database resilience using Room's &lt;code&gt;withTransaction&lt;/code&gt; blocks across all multi-account balance mutations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Horror of Partial Batch Writes
&lt;/h2&gt;

&lt;p&gt;To understand what a partial write looks like, examine what happens during a standard inter-account transfer of 20,000 rupees from a checking account to a savings account.&lt;/p&gt;

&lt;p&gt;Under the hood, this transfer requires three separate database operations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;UPDATE accounts SET currentBalance = currentBalance - 20000 WHERE id = 1&lt;/code&gt; (Checking)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UPDATE accounts SET currentBalance = currentBalance + 20000 WHERE id = 2&lt;/code&gt; (Savings)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;INSERT INTO transactions (...) VALUES (...)&lt;/code&gt; (The Transfer Audit Log)
&lt;/li&gt;
&lt;/ol&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph FailureScenario["The Partial Write Catastrophe"]
        direction TB
        Op1["1. Debit Checking: 20,000 (SUCCESS)"] --&amp;gt; Op2["2. Android Process Killed / Device Dies (HALT!)"]
        Op2 -.-&amp;gt; Op3["3. Credit Savings: 20,000 (NEVER EXECUTED)"]
        Op2 -.-&amp;gt; Op4["4. Write Transfer Record (NEVER EXECUTED)"]
        Op1 --&amp;gt; Result["Result: 20,000 rupees vanished into thin air!
Checking is debited, Savings never credited, zero transaction record!"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If these three queries execute as independent SQLite statements without an enclosing transaction boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 1 completes: 20,000 rupees leaves checking.&lt;/li&gt;
&lt;li&gt;The phone's battery dies or the OS kills the process before Step 2 executes.&lt;/li&gt;
&lt;li&gt;When the user restarts their phone, the 20,000 rupees is missing from checking, was never credited to savings, and no transaction record exists to explain what happened.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user has suffered digital data loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: ACID Atomicity with &lt;code&gt;withTransaction&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In computer science, ACID guarantees that database transactions are &lt;strong&gt;Atomic, Consistent, Isolated, and Durable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In Android Room, transactional atomicity across asynchronous Kotlin Coroutines is achieved using the &lt;code&gt;withTransaction&lt;/code&gt; extension function on &lt;code&gt;RoomDatabase&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph TransactionBoundary["Room withTransaction { ... } Boundary"]
        direction TB
        Begin["BEGIN EXCLUSIVE TRANSACTION"] --&amp;gt; Step1["1. Debit Checking (-20,000)"]
        Step1 --&amp;gt; Step2["2. Credit Savings (+20,000)"]
        Step2 --&amp;gt; Step3["3. Insert Transfer Entity"]
        Step3 --&amp;gt; Step4["4. Update Learned Merchant / Category Rules"]
        Step4 --&amp;gt; Commit["COMMIT TRANSACTION"]
    end
    Commit --&amp;gt;|"Success"| LiveState["All Rows Committed Together"]
    Step2 -.-&amp;gt;|"Process Killed / Error"| Rollback["SQLite Automatic Rollback:
Database Reverts to Exact Pre-Transfer State"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;When you wrap operations inside &lt;code&gt;appDatabase.withTransaction { ... }&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Room acquires an exclusive transaction lock on the underlying SQLite connection.&lt;/li&gt;
&lt;li&gt;All SQL insert, update, and delete queries executed inside the lambda participate in that single atomic transaction.&lt;/li&gt;
&lt;li&gt;If any step throws an exception, or if the process dies midway, SQLite's write-ahead log (WAL) rolls back every single modification upon recovery.&lt;/li&gt;
&lt;li&gt;Either all four steps succeed together, or none of them do. It is mathematically impossible for an account to be debited without the companion account being credited.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Sprint H2 Audit: Eliminating the Gaps
&lt;/h2&gt;

&lt;p&gt;During FinLedger's Hardening Sprint H2 (&lt;code&gt;commit b0ac8e2&lt;/code&gt;), we conducted an exhaustive audit of all database writes across the entire codebase.&lt;/p&gt;

&lt;p&gt;We identified and resolved three areas where partial writes could previously occur:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. CSV Data Import
&lt;/h3&gt;

&lt;p&gt;Previously, importing a large CSV spreadsheet containing 500 transactions processed and inserted rows in small incremental chunks. If the user cancelled midway or a single malformed row caused a parser error, the database was left half-imported.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resolution&lt;/strong&gt;: The entire CSV parse-and-persist phase was wrapped in a single &lt;code&gt;withTransaction&lt;/code&gt; block paired with a pre-import safety snapshot. If row 499 fails validation, all previous 498 rows are instantly rolled back, leaving the database completely pristine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Multi-Account Loan Settlements
&lt;/h3&gt;

&lt;p&gt;When a friend repays multiple loans simultaneously, the repayment must credit the user's bank account, reduce multiple loan receivables, and update the person's directory status.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resolution&lt;/strong&gt;: The entire repayment cascade was unified under &lt;code&gt;LendingRepository&lt;/code&gt; inside &lt;code&gt;withTransaction&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Credit Card Waterfall Payment Allocations
&lt;/h3&gt;

&lt;p&gt;Allocating a card payment across multiple historical statements requires updating the bank balance, card liability balance, and multiple statement payment tracking entities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resolution&lt;/strong&gt;: All statement status transitions (&lt;code&gt;UNPAID&lt;/code&gt; $\to$ &lt;code&gt;PARTIALLY_PAID&lt;/code&gt; $\to$ &lt;code&gt;FULLY_PAID&lt;/code&gt;) execute within the same atomic boundary as the bank payment transfer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Concurrency and SQLite Lock Contention
&lt;/h2&gt;

&lt;p&gt;While &lt;code&gt;withTransaction&lt;/code&gt; guarantees safety, holding a database transaction lock for too long can block other threads and cause UI stutter.&lt;/p&gt;

&lt;p&gt;To ensure high performance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We keep long-running non-database work (such as JSON parsing, cryptographic key derivation, and image compression) strictly &lt;strong&gt;outside&lt;/strong&gt; the transaction block.&lt;/li&gt;
&lt;li&gt;We prepare all entity data in memory first, enter &lt;code&gt;withTransaction&lt;/code&gt;, execute rapid batched SQL queries, and exit the transaction in less than 5 milliseconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;In mobile engineering, you must design for failure. Your app will be interrupted, backgrounded, and killed.&lt;/p&gt;

&lt;p&gt;Wrapping all multi-row and multi-account balance mutations inside atomic &lt;code&gt;withTransaction&lt;/code&gt; blocks is the non-negotiable prerequisite for building a personal finance ledger that never loses a user's money.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Your Finance App Needs a Reconciliation Engine — Mine Nearly Didn't Have One</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Tue, 03 Nov 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/your-finance-app-needs-a-reconciliation-engine-mine-nearly-didnt-have-one-11bo</link>
      <guid>https://dev.to/devanshu_patil/your-finance-app-needs-a-reconciliation-engine-mine-nearly-didnt-have-one-11bo</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3jbeow4y0xzlhl947njc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3jbeow4y0xzlhl947njc.png" alt="Your Finance App Needs a Reconciliation Engine — Mine Nearly Didn't Have One" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No matter how disciplined you are with your personal finance tracker, physical reality will eventually collide with your digital ledger.&lt;/p&gt;

&lt;p&gt;You open your mobile banking app on the first of the month and see an ending balance of &lt;strong&gt;43,210 rupees&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;You open your expense tracking app and look at that same bank account, only to find that your app displays &lt;strong&gt;44,150 rupees&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;There is a discrepancy—a delta ($\Delta$) of &lt;strong&gt;940 rupees&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Where did it come from? &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the bank quietly deduct an annual debit card fee or an SMS alert charge?&lt;/li&gt;
&lt;li&gt;Did you receive a small quarterly interest credit that you forgot to record?&lt;/li&gt;
&lt;li&gt;Did you buy fresh fruit at a roadside stall with cash that you forgot to log?&lt;/li&gt;
&lt;li&gt;Did an automated bank notification fail to deliver due to poor mobile network coverage?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If an application provides no way to reconcile this drift, users are forced into terrible workarounds: they either manually alter opening balances, invent fake transactions, or give up on tracking altogether.&lt;/p&gt;

&lt;p&gt;Here is how FinLedger solves balance drift by introducing a formal, double-entry &lt;strong&gt;Bank Statement Reconciliation Engine&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With Naive "Balance Overwrite"
&lt;/h2&gt;

&lt;p&gt;When commercial finance apps let users fix a mismatched balance, their default solution is a crude text field: "Enter new balance."&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph NaiveOverwrite["The Naive Overwrite Problem"]
        direction TB
        AppBal["App Balance: 44,150"] --&amp;gt; UserTypes["User Types: 43,210"]
        UserTypes --&amp;gt; BlindUpdate["Blind SQL Mutation:
UPDATE accounts SET balance = 43210"]
        BlindUpdate --&amp;gt; Consequences["Consequences:
1. 940 rupees vanished without explanation
2. Historical audit trail broken
3. Monthly expense charts distorted
4. Future reconciliation impossible"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Blindly overwriting the database balance breaks the foundation of double-entry accounting:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Vanished Money&lt;/strong&gt;: 940 rupees has disappeared without a paper trail. You cannot audit where your wealth went.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken Historical Parity&lt;/strong&gt;: If the app ever performs a full historical recalculation (such as replaying transactions during a backup restore), the 940-rupee gap will reappear because there is no transaction explaining the delta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics Blind Spot&lt;/strong&gt;: If the drift was caused by recurring bank service fees, your monthly expense charts will never reflect that cost, blinding you to unnecessary bank expenses.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The FinLedger Reconciliation Engine
&lt;/h2&gt;

&lt;p&gt;In FinLedger, reconciliation is treated as an audited financial event, modeled in &lt;code&gt;DatabaseReconciliationEngine.kt&lt;/code&gt; and surfaced through the dedicated &lt;code&gt;ReconciliationScreen.kt&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph ReconEngine["Reconciliation Workflow"]
        direction TB
        BankState["1. Real Bank Statement Balance:
User enters physical statement total (43,210)"] --&amp;gt; Diff["2. Engine Calculates Delta:
Delta = Actual (43,210) - Ledger (44,150) = -940"]
        Diff --&amp;gt; Category["3. Structured Adjustment Classification:
Bank Fee / Interest / Cash / Missing Tx"]
        Category --&amp;gt; Post["4. Generate Companion Adjustment Transaction:
Atomically posted via Posting.kt"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. Delta Calculation
&lt;/h3&gt;

&lt;p&gt;When the user opens the Reconciliation hub for an account:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The screen displays the current authoritative ledger balance (e.g. 44,150 rupees).&lt;/li&gt;
&lt;li&gt;The user inputs their actual bank statement balance (43,210 rupees).&lt;/li&gt;
&lt;li&gt;The engine calculates the exact discrepancy:
$$\Delta = \text{Actual Statement Balance} - \text{Current Ledger Balance}$$&lt;/li&gt;
&lt;li&gt;If $\Delta = 0.00$, the screen confirms that the account is in 100% mathematical harmony.&lt;/li&gt;
&lt;li&gt;If $\Delta \neq 0.00$, the engine initiates structured adjustment workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Structured Adjustment Transactions
&lt;/h3&gt;

&lt;p&gt;Rather than blindly overwriting the account row, FinLedger generates a formal &lt;strong&gt;Adjustment Transaction&lt;/strong&gt; categorized by the real-world cause:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Discrepancy Direction&lt;/th&gt;
&lt;th&gt;Structured Adjustment Type&lt;/th&gt;
&lt;th&gt;Accounting Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Negative ($\Delta &amp;lt; 0$)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Bank Service Charge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recorded as an operational expense categorized under "Banking &amp;amp; Fees".&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Negative ($\Delta &amp;lt; 0$)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unrecorded Cash / Expense&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recorded as an operational expense categorized under the user's chosen budget category.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Positive ($\Delta &amp;gt; 0$)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Interest Credited&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recorded as operational income categorized under "Interest &amp;amp; Dividends".&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Positive ($\Delta &amp;gt; 0$)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Unrecorded Deposit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recorded as operational income with clear reconciliation audit notes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  3. Atomic Ledger Alignment
&lt;/h3&gt;

&lt;p&gt;The generated adjustment transaction flows through the authoritative domain engine (&lt;code&gt;Posting.kt&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It updates the account balance to match the physical statement balance down to the exact paisa.&lt;/li&gt;
&lt;li&gt;It records an immutable transaction row timestamped with the reconciliation date.&lt;/li&gt;
&lt;li&gt;It attaches an explicit note: &lt;em&gt;"Reconciliation adjustment for statement balance 43,210 (Delta: -940.00)"&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Whole-App Database Reconciliation Sweep
&lt;/h2&gt;

&lt;p&gt;Beyond individual bank accounts, FinLedger v2.0.2 introduced a whole-app recalculation and reconciliation engine (&lt;code&gt;commit 271b10c&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users can trigger an on-demand integrity audit across all accounts simultaneously.&lt;/li&gt;
&lt;li&gt;The engine sweeps every active and archived account, recalculates total postings from day zero, checks for foreign key integrity, and verifies that Available Spendable Money and Total Net Worth align with absolute precision.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;A personal finance ledger must be reconcilable against the real banking system.&lt;/p&gt;

&lt;p&gt;By turning balance discrepancies into structured, categorized adjustment transactions rather than silent database overwrites, FinLedger preserves a complete historical audit trail and ensures that your numbers always reflect physical truth.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Daily Feed, Monthly Rollup, Yearly Grid: Building All Three From One Query</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Tue, 03 Nov 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/daily-feed-monthly-rollup-yearly-grid-building-all-three-from-one-query-dhp</link>
      <guid>https://dev.to/devanshu_patil/daily-feed-monthly-rollup-yearly-grid-building-all-three-from-one-query-dhp</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkpwskhi9juy6y50b86zo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkpwskhi9juy6y50b86zo.png" alt="Daily Feed, Monthly Rollup, Yearly Grid: Building All Three From One Query" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When people look at their personal finances, their perspective changes depending on the question they are asking:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At 2:00 PM on a Tuesday, they ask: &lt;em&gt;"Did my salary arrive today, and what did I spend on lunch?"&lt;/em&gt; (A &lt;strong&gt;Daily&lt;/strong&gt; perspective).&lt;/li&gt;
&lt;li&gt;On the last day of the month, they ask: &lt;em&gt;"Did I save money this month, and why was my restaurant bill so high?"&lt;/em&gt; (A &lt;strong&gt;Monthly&lt;/strong&gt; perspective).&lt;/li&gt;
&lt;li&gt;At tax season or on New Year's Eve, they ask: &lt;em&gt;"How did my overall wealth and annual spending trend across the last 12 months?"&lt;/em&gt; (A &lt;strong&gt;Yearly&lt;/strong&gt; perspective).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most finance applications force you into a single fixed view—usually an infinite scrolling list or a rigid monthly calendar.&lt;/p&gt;

&lt;p&gt;In FinLedger, we engineered a unified, multi-period ledger architecture that allows users to glide seamlessly between Daily feeds, Monthly summaries, and a comprehensive 12-Month Yearly matrix with zero swipe lag.&lt;/p&gt;

&lt;p&gt;Here is how that reactive multi-period data pipeline is built in Jetpack Compose and Kotlin Coroutines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Temporal Horizons
&lt;/h2&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph MultiPeriodArchitecture["The Multi-Period Perspective Hierarchy"]
        direction TB
        Daily["1. Daily View:
Granular audit feed, date headers, daily net burn, sticky C/F balances"]
        Monthly["2. Monthly View:
Calendar grouping, category spending breakdown, cash flow comparison"]
        Yearly["3. Yearly Matrix:
12-Month tabular grid (Jan - Dec), monthly income vs expense, running balance"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  1. The Daily Feed: Micro-Auditability
&lt;/h3&gt;

&lt;p&gt;The Daily view is designed for operational precision.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactions are grouped chronologically with high-contrast sticky day headers.&lt;/li&gt;
&lt;li&gt;Each date header displays the daily net cash flow (e.g., "+45,000" or "-1,850").&lt;/li&gt;
&lt;li&gt;The summary top card displays the exact Carried-Forward (C/F) balance from all prior days, grounding today's spend in real liquidity.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The Monthly View: Habit &amp;amp; Budget Analysis
&lt;/h3&gt;

&lt;p&gt;The Monthly view shifts focus from individual transactions to spending habits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Groups transactions across the entire calendar month.&lt;/li&gt;
&lt;li&gt;Integrates with category budgets to show real-time burn rates and pacing alerts.&lt;/li&gt;
&lt;li&gt;Displays the monthly cash flow comparison: total operational income vs total operational expenditure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. The 12-Month Yearly Matrix: High-Level Solvency
&lt;/h3&gt;

&lt;p&gt;The Yearly view solves the "long-term visibility" problem using a custom 12-month financial matrix table (&lt;code&gt;YearlyMatrixTable.kt&lt;/code&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Renders a clean 12-row tabular grid from January to December.&lt;/li&gt;
&lt;li&gt;Each row displays three authoritative columns: &lt;strong&gt;Income&lt;/strong&gt;, &lt;strong&gt;Expense&lt;/strong&gt;, and &lt;strong&gt;Ending Running Balance&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Clicking any row in the annual matrix instantly navigates the user directly to that month's detailed transaction feed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Unified Data Pipeline: One Flow to Rule Them All
&lt;/h2&gt;

&lt;p&gt;A common architectural mistake is creating separate, independent database queries and ViewModels for each time period: a &lt;code&gt;DailyViewModel&lt;/code&gt;, a &lt;code&gt;MonthlyViewModel&lt;/code&gt;, and a &lt;code&gt;YearlyViewModel&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That approach leads to duplicate code, divergent calculation logic, and jarring screen transitions.&lt;/p&gt;

&lt;p&gt;In FinLedger, all three perspectives are powered by a single reactive coroutine pipeline in &lt;code&gt;LedgerViewModel&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph Pipeline["Unified Reactive Pipeline"]
        direction TB
        Inputs["User Inputs:
_selectedPeriod (Daily/Monthly/Yearly)
_currentDate (Epoch Timestamp)
_filterState (Accounts, Categories)
_searchQuery (Debounced)"] --&amp;gt; Combine["Kotlin Flow combine()"]
        RoomDB[("Room Database
(All Accounts &amp;amp; Transactions)")] --&amp;gt; Combine
        Combine --&amp;gt; Worker["Dispatchers.Default:
Compute C/F Balance, Partition Date Groups, Build 12-Month Matrix"]
        Worker --&amp;gt; State["Immutable LedgerUiState"]
        State --&amp;gt; UI["Jetpack Compose Screen
(Instant Recomposition)"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;ol&gt;
&lt;li&gt;The ViewModel maintains lightweight reactive state holders for &lt;code&gt;_selectedPeriod&lt;/code&gt; (Daily, Monthly, Yearly), &lt;code&gt;_currentDate&lt;/code&gt;, and &lt;code&gt;_filterState&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A single &lt;code&gt;combine&lt;/code&gt; operator merges these parameters with raw database streams from Room.&lt;/li&gt;
&lt;li&gt;The transformation pipeline evaluates the active period and offloads all mathematical aggregation to &lt;code&gt;Dispatchers.Default&lt;/code&gt;:

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;Daily&lt;/code&gt;: It slices transactions for that specific day and computes prior C/F balance.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;Monthly&lt;/code&gt;: It filters transactions between day 1 and the last day of that month (&lt;code&gt;LocalDate.lengthOfMonth()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;Yearly&lt;/code&gt;: It evaluates the entire calendar year and constructs the 12-month &lt;code&gt;YearlyMatrixRow&lt;/code&gt; objects in a single pass.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The pipeline emits an immutable &lt;code&gt;LedgerUiState&lt;/code&gt; consumed directly by the Jetpack Compose user interface via &lt;code&gt;collectAsStateWithLifecycle&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Smooth Gestures: The Pager Navigation Shell
&lt;/h2&gt;

&lt;p&gt;To switch between time periods and months, the user interface uses a horizontal swipe gesture.&lt;/p&gt;

&lt;p&gt;When a user swipes left or right:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changing the date simply updates &lt;code&gt;_currentDate&lt;/code&gt; in the ViewModel.&lt;/li&gt;
&lt;li&gt;The reactive pipeline recomputes state in under 4 milliseconds on a background thread.&lt;/li&gt;
&lt;li&gt;Jetpack Compose uses structural keys on list items (&lt;code&gt;items(transactions, key = { it.id })&lt;/code&gt;) to animate changes fluidly without re-measuring the entire layout.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Preserving Invariants Across Time Horizons
&lt;/h2&gt;

&lt;p&gt;Regardless of which period the user is viewing, the underlying financial math never drifts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal transfers contribute exactly 0.00 to monthly and yearly income/expense totals.&lt;/li&gt;
&lt;li&gt;Credit card debt is subtracted from liquid cash across all views to preserve the Available Spendable Money invariant.&lt;/li&gt;
&lt;li&gt;Opening balances remain balance sheet equity snapshots, never leaking into annual revenue summaries.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Financial clarity requires adjusting the zoom level without losing mathematical truth.&lt;/p&gt;

&lt;p&gt;By powering Daily feeds, Monthly summaries, and 12-Month Yearly matrices through a single, background-computed reactive pipeline, FinLedger delivers seamless, instant transitions across all time horizons without dropped frames or accounting discrepancies.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Deleting a Transaction Should Be Surgical — Here's How to Do It Right</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Mon, 02 Nov 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/deleting-a-transaction-should-be-surgical-heres-how-to-do-it-right-4b82</link>
      <guid>https://dev.to/devanshu_patil/deleting-a-transaction-should-be-surgical-heres-how-to-do-it-right-4b82</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzogwxnf6xmkterfffwut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzogwxnf6xmkterfffwut.png" alt="Deleting a Transaction Should Be Surgical — Here's How to Do It Right" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In software development, "delete" sounds simple: remove the row from the database and move on.&lt;/p&gt;

&lt;p&gt;In a relational personal finance application, however, deleting a transaction is a major architectural operation. A single transaction record often has tentacles reaching into multiple subsystems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It altered account balances that must now be refunded.&lt;/li&gt;
&lt;li&gt;It may have been an inter-account transfer connecting two completely separate bank entities.&lt;/li&gt;
&lt;li&gt;It may have had a receipt photo stored in private app storage.&lt;/li&gt;
&lt;li&gt;It may have been a companion record generated by a loan repayment or a split expense.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you simply execute &lt;code&gt;DELETE FROM transactions WHERE id = ?&lt;/code&gt;, you leave behind &lt;strong&gt;accounting scars&lt;/strong&gt;: orphaned files consuming device storage, desynchronized loan balances, and corrupted account totals.&lt;/p&gt;

&lt;p&gt;Here is how FinLedger designs safe, zero-leak transaction deletion across all connected subsystems.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Web of Dependencies
&lt;/h2&gt;

&lt;p&gt;Consider what happens when a user deletes a single transfer transaction of 15,000 rupees sent from checking to a savings account six months ago.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph Dependencies["The Dependency Web of a Single Transaction"]
        direction TB
        Tx["Transaction Record (ID: 502)"]
        Source["Source Account (Checking: Deducted 15,000)"]
        Dest["Destination Account (Savings: Credited 15,000)"]
        Receipt["Receipt Attachment (File: receipt_502.jpg on Disk)"]
        Lending["Companion Lending Record (Debt Repayment Sync)"]

        Tx --- Source
        Tx --- Dest
        Tx --- Receipt
        Tx --- Lending
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If the deletion logic only removes the row from the &lt;code&gt;transactions&lt;/code&gt; table:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The checking account balance remains permanently under-reported by 15,000 rupees.&lt;/li&gt;
&lt;li&gt;The savings account balance remains permanently over-reported by 15,000 rupees.&lt;/li&gt;
&lt;li&gt;The receipt photo image file &lt;code&gt;receipt_502.jpg&lt;/code&gt; remains abandoned in &lt;code&gt;/data/user/0/com.finance.app/files/receipts/&lt;/code&gt;, silently consuming device disk space forever.&lt;/li&gt;
&lt;li&gt;If this transaction was linked to a loan repayment, the friend's outstanding debt is now permanently desynchronized from the actual ledger.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Atomic Deletion Sequence
&lt;/h2&gt;

&lt;p&gt;To eliminate orphaned data and balance drift, FinLedger executes transaction deletion through a unified multi-step workflow in &lt;code&gt;TransactionRepository&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph DeletePipeline["Unified Deletion Pipeline"]
        direction TB
        Fetch["1. Fetch Entity &amp;amp; Linked Attachments"] --&amp;gt; Postings["2. Calculate Inverted Postings"]
        Postings --&amp;gt; Recalc["3. Atomic SQLite Balance Reversal"]
        Recalc --&amp;gt; Sync["4. Sync Companion Subsystems (Lending/Split)"]
        Sync --&amp;gt; Disk["5. Clean Up Physical Files on Disk"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Step 1: Pre-Deletion Inspection
&lt;/h3&gt;

&lt;p&gt;Before touching the database, the repository queries the target transaction entity along with its associated records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identifies the source account and destination account (if a transfer).&lt;/li&gt;
&lt;li&gt;Retrieves all linked &lt;code&gt;TransactionAttachmentEntity&lt;/code&gt; rows containing local file URIs and remote Google Drive file IDs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 2: Inverting Postings and Recalculating Balances
&lt;/h3&gt;

&lt;p&gt;The engine passes the transaction to &lt;code&gt;Posting.kt&lt;/code&gt; to determine its exact historical balance impact, negating all deltas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If deleting an Expense: the source account is credited back the transaction amount.&lt;/li&gt;
&lt;li&gt;If deleting an Income: the destination account is debited the transaction amount.&lt;/li&gt;
&lt;li&gt;If deleting a Transfer: the source account is credited, and the destination account is debited.&lt;/li&gt;
&lt;li&gt;If deleting a Credit Card Purchase: the card's liability balance is reduced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both accounts are updated inside an atomic SQLite transaction block (&lt;code&gt;AppDatabase.withTransaction&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Two-Way Subsystem Synchronization
&lt;/h3&gt;

&lt;p&gt;If the deleted transaction was connected to a specialized feature, companion entities must be updated to preserve relational truth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lending &amp;amp; Debt Tracker&lt;/strong&gt;: If deleting a repayment transaction (&lt;code&gt;repay_tx_&amp;lt;id&amp;gt;&lt;/code&gt;), &lt;code&gt;syncLendingStateOnTransactionDelete&lt;/code&gt; immediately re-opens the loan receivable, increments the borrower's outstanding debt, and moves the person back from "Past Records" to "Active Records".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Split &amp;amp; Group Expenses&lt;/strong&gt;: If deleting an expense split across a group, the group's net settlement balances are dynamically recalculated to reflect that the shared bill no longer exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Physical Attachment Cleanup
&lt;/h3&gt;

&lt;p&gt;Once the database transaction commits successfully, the repository initiates filesystem cleanup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The local JPEG or PNG image files stored in the private app sandbox are deleted via standard filesystem APIs (&lt;code&gt;File.delete()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;If Google Drive sync is enabled, the remote file deletion queue notes the deleted ID so the cloud snapshot removes the receipt on the next sync cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Handling the Storage vs. Database ACID Window
&lt;/h2&gt;

&lt;p&gt;During FinLedger's formal architectural audits (documented in &lt;code&gt;FINLEDGER_FULL_APPLICATION_AUDIT.md&lt;/code&gt;), our review identified a subtle edge case common to all mobile apps:&lt;/p&gt;

&lt;p&gt;SQLite operations are ACID-compliant, but local filesystem operations are not part of the database transaction. If the app process dies precisely between the SQLite commit and the physical disk file deletion, an orphaned image file could theoretically remain on disk.&lt;/p&gt;

&lt;p&gt;To harden this boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attachment entities track their local file status explicitly.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;AttachmentRepository&lt;/code&gt; includes a background startup hygiene sweep that compares physical files in the receipts directory against active database attachment records, quietly pruning any orphan files whose companion database rows no longer exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Deleting a financial transaction is a reconciliation event, not a simple database row drop.&lt;/p&gt;

&lt;p&gt;By reversing postings across all counter-party accounts, synchronizing companion debt models, and cleaning up filesystem attachments within disciplined architectural boundaries, FinLedger guarantees that deleting a mistake leaves zero accounting scars.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>Why Editing a Transaction In-Place Is the Fastest Way to Corrupt Your Ledger</title>
      <dc:creator>DEVANSHU PATIL</dc:creator>
      <pubDate>Mon, 02 Nov 2026 04:30:00 +0000</pubDate>
      <link>https://dev.to/devanshu_patil/why-editing-a-transaction-in-place-is-the-fastest-way-to-corrupt-your-ledger-4ba7</link>
      <guid>https://dev.to/devanshu_patil/why-editing-a-transaction-in-place-is-the-fastest-way-to-corrupt-your-ledger-4ba7</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdz9usuef7vx12e5pv0zi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdz9usuef7vx12e5pv0zi.png" alt="Why Editing a Transaction In-Place Is the Fastest Way to Corrupt Your Ledger" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a standard CRUD (Create, Read, Update, Delete) application, editing a record is usually the simplest feature to implement.&lt;/p&gt;

&lt;p&gt;If a user edits a blog post title, you execute &lt;code&gt;UPDATE posts SET title = ? WHERE id = ?&lt;/code&gt;. If they update their username, you overwrite the database column. The change is isolated, self-contained, and has zero side effects on other rows.&lt;/p&gt;

&lt;p&gt;In a personal finance ledger, however, &lt;strong&gt;there is no such thing as an isolated edit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;If your application naively performs an in-place SQL update on a transaction record, your ledger balances will immediately corrupt.&lt;/p&gt;

&lt;p&gt;Here is how FinLedger architected safe transaction editing using an atomic two-phase posting reconciliation engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The In-Place Update Trap
&lt;/h2&gt;

&lt;p&gt;To understand why in-place updates fail, consider what happens when a user modifies a simple expense.&lt;/p&gt;

&lt;p&gt;Suppose on October 5th, a user enters a 2,000-rupee grocery expense paid from their HDFC Bank checking account. At that moment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HDFC checking balance is debited by 2,000 rupees.&lt;/li&gt;
&lt;li&gt;Monthly grocery spending increases by 2,000 rupees.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph NaiveUpdate["The Naive In-Place SQL Update"]
        direction TB
        E1["User Changes: Amount (2,000 -&amp;gt; 2,500) &amp;amp; Account (HDFC -&amp;gt; Cash)"] --&amp;gt; E2["Run In-Place SQL:
UPDATE transactions SET amount = 2500, account_id = 'cash' WHERE id = 101"]
        E2 --&amp;gt; E3["Result:
HDFC Account STILL MISSING 2,000!
Cash Account NEVER DEBITED!
Ledger Corrupted Forever!"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;If the developer simply runs &lt;code&gt;UPDATE transactions SET amount = 2500, account = 'cash'&lt;/code&gt;, what happens to the account balances?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The HDFC checking account is still missing the original 2,000 rupees that was deducted on October 5th.&lt;/li&gt;
&lt;li&gt;The Cash wallet was never debited by the new 2,500-rupee amount.&lt;/li&gt;
&lt;li&gt;The database is now out of sync with physical reality.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two-Phase Reconciliation Solution
&lt;/h2&gt;

&lt;p&gt;To ensure absolute mathematical consistency, FinLedger treats every transaction edit as a formal &lt;strong&gt;two-phase financial reconciliation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Rather than attempting to calculate complex multi-variable deltas between the old and new states, the repository executes a clean inverse-then-post sequence:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    subgraph EditCycle["Two-Phase Invariant Pipeline"]
        direction TB
        OldTx["1. Fetch Stored Original Transaction"] --&amp;gt; Inverse["Phase 1: Invert Original Postings
(Refund original accounts)"]
        NewTx["2. Validate New Transaction Inputs"] --&amp;gt; Forward["Phase 2: Apply New Postings
(Debit/Credit updated accounts)"]
        Inverse &amp;amp; Forward --&amp;gt; Atomic["Execute Atomically in SQLite:
AppDatabase.withTransaction { ... }"]
    end&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Phase 1: Inverse Unwinding
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;The engine retrieves the existing, unedited transaction entity from the database.&lt;/li&gt;
&lt;li&gt;It feeds the original transaction through &lt;code&gt;Posting.kt&lt;/code&gt; to determine its exact original impact on account balances.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;At this millisecond, the ledger state is restored to exactly what it would have been if the original transaction had never occurred.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 2: Forward Application
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;The engine validates the newly edited transaction data (ensuring positive non-zero amounts, distinct transfer accounts, and valid categories).&lt;/li&gt;
&lt;li&gt;It feeds the new transaction through &lt;code&gt;Posting.kt&lt;/code&gt; to compute the new balance deltas (e.g., deducting 2,500 rupees from the Cash wallet).&lt;/li&gt;
&lt;li&gt;It updates the transaction row in the SQLite database.&lt;/li&gt;
&lt;li&gt;It recalculates the final balances for all touched accounts (both the old accounts that were refunded and the new accounts that were debited).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Guaranteeing ACID Atomicity via &lt;code&gt;withTransaction&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The critical requirement of this two-phase sequence is &lt;strong&gt;atomicity&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;In FinLedger, the entire lifecycle is encapsulated inside Room's transactional block:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph RoomTransaction["SQLite Transaction Boundary (withTransaction)"]
        direction TB
        Step1["1. Begin SQLite Transaction (BEGIN EXCLUSIVE)"] --&amp;gt; Step2["2. Revert Old Postings on Touched Accounts"]
        Step2 --&amp;gt; Step3["3. Update Transaction Table Row"]
        Step3 --&amp;gt; Step4["4. Apply New Postings on Touched Accounts"]
        Step4 --&amp;gt; Step5["5. Synchronize Companion Records (Lending / Receipts)"]
        Step5 --&amp;gt; Step6["6. Commit Transaction (COMMIT)"]
    end
    Step6 --&amp;gt;|"On Any Error / Exception"| Rollback["Automatic SQLite ROLLBACK (Zero Data Loss)"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Date Boundary Edits and Statement Snapshots
&lt;/h2&gt;

&lt;p&gt;A further complication arises when the transaction's &lt;strong&gt;date&lt;/strong&gt; is edited across billing boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaway
&lt;/h2&gt;

&lt;p&gt;Editing financial records requires respecting historical cause and effect.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Explore FinLedger on Google Play: &lt;a href="https://play.google.com/store/apps/details?id=com.devanshufinance.app" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=com.devanshufinance.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>kotlin</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
  </channel>
</rss>
