Architecture diagrams always look beautiful in blog posts.
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.
Then you write 100,000 lines of production code over several months.
Features are added rapidly; release deadlines loom; edge cases arise. Slowly, boundaries begin to erode:
- A ViewModel starts executing raw SQL queries to save five minutes.
- A Repository starts formatting user-facing date strings.
- A composable screen calls three different repositories directly because creating an aggregator feels like boilerplate.
Recently, we completed an exhaustive, 360-file architectural audit of FinLedger (documented in FINLEDGER_FULL_APPLICATION_AUDIT.md).
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.
The Architectural Blueprint
FinLedger is organized according to Clean Architecture and MVI/MVVM presentation patterns:
flowchart TD
subgraph UI_Layer["1. Presentation Layer (Jetpack Compose)"]
direction TB
Screens["Screens & Composables:
LedgerScreen, DashboardScreen, CreditCardDetailScreen"]
Tokens["Design System Tokens (Colors, Shapes, Elevation)"]
end
subgraph VM_Layer["2. ViewModel Layer (StateFlow & Coroutines)"]
VMs["ViewModels:
LedgerViewModel, BudgetViewModel, VaultViewModel"]
Pipelines["Reactive Pipelines & 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 & Validation Rules"]
end
subgraph Data_Layer["4. Data Layer (Persistence & Security)"]
Repos["Specialized Repositories:
TransactionRepo, AccountRepo, LendingRepo, VaultRepo"]
DB[("Room + SQLCipher Encrypted SQLite
(33 Entities, 24 DAOs, Migrations 1 -> 24)")]
Security["KeyStore & SecurityManager (PBKDF2)"]
end
UI_Layer -->|"User Intents"| VM_Layer
VM_Layer -->|"Immutable StateFlow"| UI_Layer
VM_Layer -->|"Domain Commands"| Repos
Repos -->|"Delegates Calculations"| Domain_Layer
Repos <-->|"withTransaction ACID"| DB
The Triumphs of This Design
-
Isolated Accounting Math: Consolidating all posting rules into
Posting.ktandLedgerEngine.ktcompletely eliminated balance calculation drift between UI screens and background workers. -
Lightning-Fast JVM Testing: Because the domain engine contains zero
android.*imports, all 1,570 automated unit tests execute directly on the JVM in under 12 seconds. - Database Security Isolation: SQLCipher page-level encryption and KeyStore master key management remain completely encapsulated in the data layer; UI screens have zero awareness of cryptographic ciphers.
Where Architecture Eroded: The "God Object" Problem
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.
Our October audit uncovered a stark reality: the top 10 largest files accounted for over 17,900 lines of code—nearly 18% of the entire codebase!
| Monolithic File | Line Count | Primary Responsibility | Architectural Flaw |
|---|---|---|---|
BackupRestoreManager.kt |
3,043 LOC | Encrypted backup/restore | Acts as codec, disk unpacker, and cloud sync coordinator simultaneously. |
VaultDialogs.kt |
2,813 LOC | Secret Fund UI modals | Houses 14 distinct setup dialogs and location sliders in a single file. |
ModernAddTransactionScreen.kt |
1,985 LOC | Transaction entry UI | Mixes layout composables with complex input validation and category auto-selection. |
DatabaseReconciliationEngine.kt |
1,502 LOC | Whole-app ledger recalculation | Contains both audit algorithms and direct database update routines. |
SplitGroupDetailScreen.kt |
1,497 LOC | Group expenses UI | Combines 3 distinct tab layouts (Bills, Balances, History) in one file. |
Why does this happen?
It happens because convenience compounds. When adding a new backup feature, it is always faster to add a private function to an existing BackupRestoreManager.kt than to design a new modular driver interface. Over months, a 400-line class quietly becomes a 3,000-line monolith.
The Storage vs. Database ACID Boundary Window
The audit also exposed a subtle boundary leak between SQLite and the Android filesystem.
When a user deletes a transaction with receipt photos:
- The database rows in the
transactionsandtransaction_attachmentstables delete within an atomic Room transaction (withTransaction). - But deleting the physical JPEG files from disk (
File.delete()) must happen outside SQLite, because the local filesystem is not managed by SQLite's transaction journal.
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.
Identifying this leak allowed us to implement an automated startup hygiene sweep in AttachmentRepository to quietly prune orphaned disk files.
The Modular Refactoring Roadmap for v2.1.2
A real engineering architecture is not a static monument; it is a living system that requires continuous maintenance.
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 & SHA-256 Envelope"]
Cipher["BackupVaultCipher:
AES-256-GCM Cryptographic Engine"]
Runner["LocalRestoreRunner:
Atomic Filesystem Unpacking & Rollback"]
Driver["CloudSyncDriver:
Google Drive REST v3 Transport"]
end
Monolith ==>|"Refactored Into"| Modular
Based on our architectural audit, our refactoring plan decomposes our largest classes into single-responsibility drivers:
- Splitting
BackupRestoreManager.ktintoBackupPayloadCodec(pure serialization),BackupVaultCipher(cryptography),LocalRestoreRunner(filesystem rollback), andGoogleDriveSyncDriver(network transport). - Decomposing
VaultDialogs.ktinto 6 modular dialog composables. - Decoupling
ModernAddTransactionScreen.ktinto presentation-only widgets and a standaloneTransactionInputValidator.
Key Takeaway
Clean Architecture is not about achieving ideological purity; it is about managing cognitive complexity and change resistance.
When your codebase grows, audit your boundaries regularly. Acknowledge your monoliths honestly, and refactor before file concentration turns your architecture into legacy code.
FinLedger is a privacy-first, offline-first personal finance application for Android that models bank accounts, credit cards, and cash without double-counting.
Explore FinLedger on Google Play: https://play.google.com/store/apps/details?id=com.devanshufinance.app

Top comments (0)