Architecture Overview
Scope: owns the system map, layer diagram, module map, and the descriptive ADR index table.
What AutoBudget is
AutoBudget is an offline-first Android budget tracker for the Malaysian
market. A NotificationListenerService captures bank and e-wallet push
notifications, a pure-Kotlin parser engine turns them into transactions, a
keyword classifier categorizes them, and everything is stored in a
SQLCipher-encrypted Room database and shown in a Jetpack Compose UI.
There is no server, no cloud, and no network permission in the app.
System context
flowchart LR
subgraph Device
subgraph "Bank & wallet apps (user-enrolled)"
B1[Maybank2u / MAE]
B2[TNG eWallet / GrabPay / Boost]
B3[BigPay / CIMB / PBe / RHB / ShopeePay / Wise]
end
subgraph AutoBudget
L[NotificationListenerService]
P[CapturePipeline]
D[(SQLCipher Room DB)]
U[Compose UI]
K[Android Keystore]
end
subgraph "System services"
NL[NotificationManagerService]
end
end
B1 -->|notification| NL
B2 -->|notification| NL
B3 -->|notification| NL
NL -->|posted + replay| L
L --> P
P --> D
D <--> U
D -.passphrase encrypted.-> K
U -->|user actions| D
Layered architecture
flowchart TB
subgraph UI["ui/ — Compose screens + ViewModels"]
NAV[AutoBudgetNavHost]
VM[Home / Transactions / Budgets / Reports / Settings / Onboarding / Unmatched / Lock ViewModels]
SHARED[shared components: TransactionRow, MonthSelector, EmptyState, SectionHeader, CategoryIcon]
end
subgraph DOMAIN["domain/ — classification seam"]
CC[CategoryClassifier interface]
KC[KeywordClassifier]
end
subgraph PARSER["parser/ — pure Kotlin, no Android deps"]
PE[ParserEngine]
AP[AmountParser]
ND[DirectionDetector / StatusDetector]
EJ[EmbeddedJsonParser]
NK[NotificationKey]
MN[MerchantNormalizer]
end
subgraph LISTENER["listener/ — capture"]
NLS[TransactionNotificationListener]
EX[NotificationExtractor]
NF[NotificationFilter]
CP[CapturePipeline]
TN[TransactionNotifier / NotificationGate]
LOC[LocationProvider]
end
subgraph DATA["data/ — Room, repositories"]
DB[AppDatabase + SQLCipher]
ENT[Entities]
DAO[DAOs]
REPO[Repositories]
SEED[DefaultCategories + BankRuleSets]
end
subgraph COMMON["common/ — money, export, security, time"]
MONEY[Currency / MoneyFormatter / MoneyText]
EXP[ExportCodec / UnmatchedJsonCodec / UnmatchedExportCodec]
LOCK[AppLockManager / LockClock]
TR[TimeRange]
NA[NotificationAccess]
end
NAV --> VM
VM --> REPO
VM --> COMMON
VM --> SHARED
REPO --> DAO
DAO --> ENT
ENT --> DB
SEED --> DAO
CP --> NF
CP --> PE
CP --> CC
CP --> REPO
CP --> NK
CP --> TN
NLS --> EX
NLS --> LOC
NLS --> CP
PE --> AP
PE --> ND
PE --> EJ
CC --> MN
CC --> DAO
MONEY --> AP
LOCK --> REPO
Layering rule: parser/ and the money/export codecs in common/ are pure
Kotlin — no Android imports — so they are unit-testable on the JVM without
Robolectric. Everything else may depend on Android. Nothing may depend upward:
parser/ never imports ui/; ui/ talks to data/ only through
repositories and ViewModels.
Module map
| Package | Responsibility | Key types |
|---|---|---|
com.zharif.autobudget |
App entry, main activity, debug fixture harness | AutoBudgetApp, MainActivity, DebugFixtureReceiver (debug) |
.listener |
System-bound capture | TransactionNotificationListener, CapturePipeline, NotificationExtractor, NotificationFilter, TransactionNotifier, TransactionNotificationText, NotificationGate, LocationProvider |
.parser |
Pure parsing: rules, amount/currency, direction/status, JSON payloads, dedupe keys, merchant normalization | ParserEngine, ParserRule, AmountParser, Currency (via common), EmbeddedJsonParser, NotificationKey, MerchantNormalizer, BankRuleSets |
.domain.category |
Classification seam + keyword implementation | CategoryClassifier, KeywordClassifier |
.data |
Room + SQLCipher, entities, DAOs, repositories, seeds | AppDatabase, Entities, Daos, DatabasePassphrase, TransactionRepository, SettingsRepository, … |
.di |
Hilt wiring | AppModule |
.common |
Money, export/import codecs, app lock, time ranges, FX, itemization, notification access | Currency, MoneyFormatter, MoneyText, ExportCodec, UnmatchedJsonCodec, UnmatchedExportCodec, AppLockManager, LockClock, TimeRange, Fx, Itemization, NotificationAccess |
.ui |
Compose screens + ViewModels + navigation + theme | AutoBudgetNavHost, per-feature screens/ViewModels, ui/theme, ui/components |
Capture & deduplication
The notification → transaction sequence, routing table, replay behavior and
cross-source dedupe live in
ingestion-pipeline.md; the dedupe key formula is
defined in parser.md. In short: a
bounded session-level set skips rebind re-processing, and the DB unique index
on transactions.notificationKey is the durable guarantee — replay on
connect is idempotent through both.
Why the design looks the way it does
Each decision below is formalized as a numbered ADR under
docs/adr/ with context, decision, and consequences — read the
ADR before changing behavior in its area:
| Decision | ADR |
|---|---|
occurredAt is the notification post time, no date extraction from text |
ADR-0001 |
Matched-rule provenance: ruleId FK populated via ParserRule.dbId |
ADR-0002 |
| NONE-confidence candidates route to the unmatched queue | ADR-0003 |
Rules loaded per notification from the rules table; no static fallback |
ADR-0004 |
New starter rules re-seeded onto existing installs once per seed version (sourceId + rules_seed_version) |
ADR-0019 |
| Keyword pre-filter, masked routing, session dedupe, 48h replay | ADR-0005 |
Filtering is in-memory (TransactionFilter over a month list) |
ADR-0006 |
Category overrides live behind the classification module, keyed (package, merchantNormalized) |
ADR-0007 |
Tracked-banks CSV lives in SettingsRepository with legacy renames |
ADR-0008 |
Money is Long minor units only; single Currency tables |
ADR-0009 |
| SQLCipher + Android Keystore passphrase; backups disabled | ADR-0010 |
16 KB page-size support via sqlcipher-android migration |
ADR-0011 |
| App lock: salted hash, lockout, biometric, auto-lock | ADR-0012 |
Hand-rolled Compose Canvas charts, no chart dependency |
ADR-0013 (superseded by ADR-0028) |
| Report charts via ComposeCharts 1.0.0 (donut + columns), app-side labels/semantics | ADR-0028 |
| Categorical chart palette from full-tone accents, ≥3:1 card contrast; sub-2% slices merge into "Other" | ADR-0029 |
| Offline vector basemap for the location picker: bundled z≤9 PMTiles overview, user-imported street-level packs, MapLibre rendering, no network | ADR-0030 |
| Debug-only fixture injection harness | ADR-0014 |
| Offline-first, no INTERNET permission | ADR-0015 |
| Silent transaction-captured notification + parse-source tracking | ADR-0016 |
| Editor edits currency (reformat preserves minor units), date/time, and previews export JSON | ADR-0017 |
| Play Store distribution: Play App Signing upload key, AAB + APK, opt-in internal-track upload | ADR-0018 |
| Cross-source dedupe: bank preferred over a Google Wallet mirror (same card charge recorded once) | ADR-0020 |
Captured-location columns ready on transactions/unmatched; capture + resolution deferred |
ADR-0021 |
| Custom category CRUD with category-aware delete | ADR-0022 |
| Full Material icon catalog with searchable lazy picker | ADR-0023 |
| Manual transaction itemization: child rows, signed costs, non-blocking mismatch warning | ADR-0024 |
| Offline FX conversion for foreign-currency aggregation | ADR-0025 |
| Location capture (opt-in) via cached last-known fix + offline coordinate picker | ADR-0026 |
| Background location capture: "Allow all the time" grant + live one-shot fix with cached fallback | ADR-0034 |
| Transaction attachments: metadata in DB, bytes in app filesDir, whitelist + 10 MB cap | ADR-0027 |
Single deep-link router: whitelist autobudget:// grammar, validated prefill params, no exported link components |
ADR-0031 |
Theme-aware offline basemap: color-token style asset + Kotlin light/dark palettes via LocalDarkTheme |
ADR-0032 |
| Receipt photo capture: system camera (no CAMERA permission), auto-detected perspective crop, staged as IMAGE attachments | ADR-0033 |
Public privacy policy at https://autobudget.zharif.my/privacy/ + in-app link |
ADR-0035 |
Related docs
- Data layer — ER diagram, entities, DAOs, migrations
- Ingestion pipeline — listener, filter, pipeline sequence
- Parser engine — rules, amount parsing, confidence, golden tests
- Classification — the
CategoryClassifierseam - Money — minor units, currency tables, formatting
- UI layer — navigation graph, screens, ViewModels, adaptive layout
- Security — app lock, masking, backup policy
- Tooling — build, 16 KB support, CI, tests, release