Engineering Offline-First for Nigerian Market Conditions
The brief for MarketSave was clear: a microfinance collection app for market women across Lagos and Asaba, with real-time reporting for the loan officers managing their portfolios. The user population would be using budget Android devices on networks ranging from fast 4G in Victoria Island to 2G-equivalent EDGE in some market areas.
The business requirement was non-negotiable: the app must work when the internet doesn’t. Loan collection doesn’t stop because NCC is having a bad week.
The Architecture Decision
We chose React Native with a local-first data layer using WatermelonDB — a high-performance database for React Native built on SQLite. The architecture is:
- All reads go to the local database. The user never waits for a network call to see their data.
- All writes go to the local database first, then sync to the server when connectivity is available.
- Sync is conflict-resolved using a last-write-wins strategy with server-side timestamps. For the MarketSave use case, this is safe — two loan officers collecting from the same borrower at the same time is detected as a conflict and flagged for manual review.
The Sync Engine
The sync engine was the most complex piece of the system. Key decisions:
Incremental sync, not full sync. On startup, the app only pulls changes since the last sync timestamp. A full sync of a loan portfolio would take minutes on a slow connection. Incremental sync takes seconds.
Background sync. The sync engine runs on a background timer every 30 seconds when online, and on every network reconnection event. Users don’t trigger sync — it just happens.
Exponential backoff on failure. If a sync attempt fails, the next attempt waits 30 seconds, then 60, then 120, then 300. This prevents the app hammering a struggling server and burning the user’s data plan.
Pending change queue. Writes that haven’t synced are displayed with a subtle clock icon on the affected record. Users know which data is local-only. This honesty about system state builds trust.
Handling Conflicts
In a collection app, the key conflict scenario is: two loan officers update the same borrower record offline, then both sync. Whose update wins?
We implemented a three-tier conflict strategy:
- Mergeable fields (notes, contact info) — both updates are preserved with timestamps, and a conflict notification is shown.
- Financial fields (balance, payment records) — server-side validation logic determines the correct state. Conflicting payments are flagged for supervisor review.
- Status fields (active/inactive, loan stage) — last write wins, with full history preserved for audit.
The supervisor review workflow for financial conflicts has never been triggered in production. In six months of operation, no two loan officers have collected from the same borrower offline simultaneously. But the system handles it correctly if they do.
Performance on Budget Devices
WatermelonDB’s SQLite foundation is fast, but React Native rendering performance on 2GB RAM devices requires care. Key optimisations:
FlatList with getItemLayout. The borrower list can have hundreds of entries. FlatList with getItemLayout virtualises properly; without it, scrolling is janky on slow devices.
Memoisation of list items. Each borrower card is wrapped in React.memo. Without this, every sync that updates any record re-renders the entire list.
Image optimisation. Borrower profile photos are stored locally at 128×128px maximum. Full-resolution photos are never stored on-device.
Results
MarketSave is processing over 10,000 loan collections per month. Average sync time on a 3G connection is under four seconds. The app works in airplane mode for as long as needed, with no data loss. Loan officer NPS is 74 — unusually high for a B2B tool.
The lesson: offline-first is not a fallback strategy. On Nigerian networks, it is the primary strategy. Build for offline and treat online as an enhancement.