A wholesale client came to us needing real inventory and billing software — barcode scanning, GST-ready invoices, purchase tracking, running ledgers. The catch: their shop floor has patchy or zero internet for large stretches of the day. Most off-the-shelf ERPs assume a live connection to a cloud backend. That assumption doesn't survive contact with a real warehouse.
This is what we actually did to build Shop Inventory Management as a fully offline-first Kotlin Android app — and the parts of the design that turned out to matter more than expected.
The first and most important architectural call was treating the on-device database — not a remote server — as the authoritative source of truth for every screen in the app. Billing, inventory counts, ledgers: all of it reads from and writes to local storage first, with zero blocking calls to a network on the critical path of any user action.
If a feature's happy path requires a network round-trip to complete, it isn't actually offline-first — it's online-first with a loading spinner you hope resolves quickly.
Product data is cached locally in full, not fetched per-scan. A scan that depends on a live lookup is a scan that fails exactly when the shop floor needs it most.
Tax calculation and PDF generation happen entirely on-device. The invoice a customer walks out with can't depend on whether the shop's router is up.
Every transaction writes to local storage immediately and atomically — no "pending sync" state that a user has to understand or worry about.
All 12+ reports compute from the local dataset directly. No report waits on a network call, because none of the data it needs lives anywhere else by default.
The naive version of "store everything locally" breaks in a specific way: if the app ever needs to run on more than one device for the same shop, you now have two sources of truth that can drift apart. We hit this early in testing — two staff members entering stock counts from different devices produced inventory numbers that didn't agree.
The fix wasn't a magic conflict-resolution algorithm. It was simpler and more honest: each device owns its own ledger of events (not final totals), and totals are always derived by replaying events, not stored as a single mutable number. Two devices can each append events locally without stepping on each other; reconciling them later is addition, not overwrite.
The lesson
Store the events that happened, not just the current state. A "current stock count" that gets overwritten is a conflict waiting to happen. A log of "+40 received, -12 sold, -3 damaged" can always be replayed into a correct total, from any device, in any order.
When the device does get connectivity, sync is a background operation that uploads the local event log and merges it with other devices' logs — it never blocks a user action waiting for that to finish, and the app is fully functional indefinitely with sync never running at all. That last part matters: for some shops, "offline-first" in practice just means "offline, full stop," and the app has to be fine with that being the permanent state.
"Offline-first" gets used loosely to mean "has a cache." The actual test is simpler: does every core user action complete successfully with the device in airplane mode, indefinitely, with no degraded experience? If the answer is anything other than yes, the architecture is online-first with offline tolerance — which is a different, much weaker thing.
Offline-first architecture is a design decision, not a checkbox — let's talk about what yours actually needs.
Get a Free Quote →