Architecture, data model, and assorted notes for anyone (mostly future me) poking around the code.
| Layer | Technology |
|---|---|
| Language | Java 25 |
| UI | Swing + FlatLaf 3.5.4 |
| Build | Gradle (Kotlin DSL) |
| Serialization | Gson 2.11.0 — plain JSON on disk, no database |
| PDF Generation | iText 8 |
| Excel Export | Python 3 + openpyxl, auto-installed into a local venv on first export |
| Packaging | jpackage — macOS DMG and Windows EXE, each built on its own platform (no cross-compiling installers) |
Everything lives under com.github.shanebeee.reconciled:
reconciled/
├── Main.java # Entry point, onboarding gate, profile resolution
├── model/ # Plain data classes (Boss, Invoice, CcaAsset, etc.)
├── storage/ # DataStorage (Preferences + paths), ProfileManager
├── util/ # Exporters, PDF/Excel generation, deduction math
└── view/ # Swing panels — one per major feature area
No database — everything is plain JSON on disk, which means it's human-readable, diffable, and trivially portable. If it's stored in iCloud Drive, sync is free.
Reconciled/
├── profiles.json # id, name, dataPath, avatarColor per profile
├── Shane_Bolenback/ # one named subfolder per user profile
│ ├── settings/
│ │ ├── employee.json
│ │ ├── bosses.json
│ │ ├── settings.json
│ │ ├── expense_categories_{year}.json
│ │ ├── tax_brackets_{year}.json
│ │ └── cca_assets.json
│ ├── logs/yyyy-MM.json
│ ├── invoices/{invoice_log.json, *.pdf}
│ ├── receipts/{year}/{month}/
│ └── km/{year}/{trips.json, odometer.json}
└── Jenny_Francisco/ # completely independent from the above
└── ...
The root directory itself defaults to ~/Library/Mobile Documents/com~apple~CloudDocs/Reconciled/
on iCloud, or ~/Reconciled/ locally, but is fully overridable during onboarding.
The saved data path lives in macOS Preferences, under the node derived from
com.github.shanebeee.reconciled.storage (backed by
~/Library/Preferences/com.github.shanebeee.plist when running as a
packaged jpackage app — a plain ./gradlew run JVM instead falls under the
generic com.apple.java.util.prefs.plist, which is worth knowing if you ever
go looking for it in the wrong plist, like I did).
ProfileManager.getRootDir() is the single source of truth for where
profiles.json lives. It checks, in order: an explicitly saved
rootDirectory preference, then a legacy fallback to the saved data
directory, then a hardcoded default. Once a root directory is saved, it's used
permanently — Main.java never re-derives it from scratch on a later launch.
Why that last point matters: an earlier version of Main.java
re-derived the root directory on every launch by string-matching the app's
folder name inside the current data path, then truncating there. It worked fine —
until a rename made that string match fail once. The fallback for a failed match was
"just use the current (possibly deep) data path as the root," which meant the very next
launch nested a new profile folder inside that path, then the launch after that
nested another one inside that — compounding one level deeper every single
time the app opened, with no error and no obvious symptom beyond "why do I have a
profile called Me/Me/Me/Me/Me/Me/Me/Me now." Storing the resolved root once and
trusting it thereafter fixed it for good.
CcaAsset is a pure computed model — there's no mutable running balance
for Undepreciated Capital Cost (UCC). Opening UCC, the year's deduction, and Closing
UCC are all derived mathematically from the asset's original cost, purchase date,
and CRA class rate, for any requested tax year. That makes it trivial to preview a
future or past year without needing to replay history.
CRA's half-year rule, as implemented:
deduction = cost × rate × 0.5deduction = openingUCC × rate, where openingUCC compounds the prior year's closing balance
Triggered when an invoice is marked Paid. Reconciled takes your year-to-date paid
invoices, annualizes that figure (extrapolates a full-year estimate from what's been
earned so far), and runs it against your configured federal and BC tax brackets plus
CPP settings to estimate what to set aside for GST, federal tax, provincial tax, and CPP.
Brackets are stored per tax year in tax_brackets_{year}.json, so historical
years stay accurate even as rates change — deliberately kept as manual configuration
rather than scraped from anywhere, since CRA rates are only published once a year anyway.
Reconciled was originally called EmployeeTimesheet, built purely to log hours and generate invoices. As expense tracking, CCA depreciation, tax set-asides, and multi-user profiles were added, it grew into a genuine bookkeeping suite — at which point the old name stopped fitting, and it was renamed to Reconciled to match what it had actually become.
# Run in development
./gradlew run
# Build fat JAR
./gradlew jar
# Package as macOS DMG (must be run on macOS)
./gradlew jpackageMac
# Package as Windows EXE installer (must be run on Windows)
./gradlew jpackageWindows
jpackage can't cross-compile installers for another OS, so each packaging task has
to run on that same platform. jpackageWindows defaults to an .exe
to avoid requiring the WiX Toolset — switch to --type msi in
build.gradle.kts if WiX is available and an .msi is preferred.
Requires Java 25+.