Building an app that assumes the power will go out
Offline-first is an architecture decision, not a feature toggle. What it changed about how we store attempts, cache audio and sync streaks.
Most apps treat offline as an error state. A request fails, a spinner hangs, an alert appears, and the user is told to check their connection, as though that were news to them. Building for Nepal makes that posture untenable within about a week of real usage.
Load-shedding, patchy coverage outside the valley and metered data are not edge cases here. They are the median conditions. So offline stopped being a feature we would add and became an assumption we designed against.
What happens when a candidate answers a question
- 1
Tap
The candidate selects an option.
- 2
Local write
The answer is committed to on-device storage before anything is sent anywhere.
- 3
Sync
When there is a connection, the attempt is uploaded. If there is not, it waits without blocking anything.
- 4
Reconcile
The server score arrives and replaces the provisional local view.
The answer is written to on-device storage first. The user interface updates from that local write. A synchronisation step later sends the attempt to the server, which scores it authoritatively and returns the result to be reconciled locally.
What that actually changed
Attempts are local first
When a candidate answers a question, that answer is written to on-device storage before anything is sent anywhere. The network call is a synchronisation step, not the write itself. If the app is killed mid-exam, nothing is lost, because nothing depended on the request succeeding.
This is straightforward until you consider scoring. We score on the server, deliberately, because a score computed on the handset is a score a motivated user can change. So the local record is the attempt, and the authoritative result arrives when the sync completes. The app shows a provisional view and reconciles it, and the reconciliation has to be right.
Content is cached deliberately, not opportunistically
A cache that fills up with whatever the user happened to open is not much use when the power goes out. Downloads are explicit: the candidate chooses a lesson set, a vocabulary deck or a listening pack, and it is stored until they remove it. Predictability matters more than cleverness when someone is relying on it.
Audio is the expensive part
Text questions are tiny. A forty-question practice set is well under a megabyte, which is nothing. Listening audio is orders of magnitude larger, and on a metered connection that is a real cost to a real person.
- Listening packs download once, over Wi-Fi by default, and play offline afterwards.
- Low data mode drops the streaming bitrate and defers images until requested.
- Nothing prefetches audio in the background. Ever. Spending someone's data without asking is not a trade-off, it is a bug.
Streaks had to work offline too
A streak that breaks because the user had no signal would punish someone for their electricity supply. So streaks are computed on-device from local activity, and the server reconciles rather than arbitrates. It is slightly more complex and it is obviously correct.
The part that took longest
Conflict resolution. A candidate signs in on a second handset, both devices hold unsynced attempts, and both are legitimate. Last-write-wins would silently discard someone's work, which for an exam attempt is unacceptable. Attempts are append-only and keyed by attempt identity, so two devices produce two records rather than one overwriting the other.
Offline as an error state
- Request fails, spinner hangs, alert appears
- The user is told to check a connection they cannot fix
- Work in progress is lost on a failed write
- Last-write-wins silently discards the loser
- Cheap to build, expensive to live with
Offline as an assumption
- Local write first, sync second, never blocking
- The interface never mentions the network unless it matters
- Killing the app mid-exam loses nothing
- Append-only records keyed by attempt identity
- Expensive to build, and impossible to retrofit later
It is not the most interesting engineering on the platform, and it is the reason nobody has ever written to us to say they lost a session.
Frequently asked questions
- Why not score on the device and sync the result?
- Because a score computed on the handset is a score a motivated user can change. Scoring stays on the server; the device holds the attempt, which is a record of what was answered rather than a claim about how well.
- How do you handle two devices with unsynced attempts?
- Attempts are append-only and keyed by attempt identity, so both devices produce records and neither overwrites the other. Last-write-wins would discard a real exam attempt, which is not an acceptable outcome.
- Can offline-first be added to an existing app later?
- It can, but it is a data-layer rewrite rather than a feature. Offline-first is a decision about where the source of truth lives, and moving that decision after launch touches almost everything.
Offline-first is not a library you install. It is a decision about where the source of truth lives, and it is very expensive to retrofit. Make it on day one or accept that you will rewrite the data layer later.