Offline-First Architecture
Keeping the app usable and correct when the network isn't — with predictable retries and no silent data loss.
An offline-first layer that lets read screens serve cached data instantly and lets writes queue safely, so flaky connectivity degrades gracefully instead of breaking the app.
Cache strategy
Reads follow a stale-while-revalidate model: the UI always renders whatever is in the local cache first, then silently reconciles with the network response when it arrives. Cache entries are tagged with a TTL and a 'source' flag (network vs. cache) so the UI can show a subtle 'showing saved data' indicator without blocking interaction.
Cache persistence is layered — an in-memory layer for the current session (fast, ephemeral) backed by AsyncStorage for cross-launch persistence. Only data the user is likely to need offline (recent lists, current cart/profile) is persisted to disk; ephemeral search results are memory-only to avoid bloating storage.
- Stale-while-revalidate reads — cache renders immediately, network reconciles in the background
- Two-tier cache: in-memory for session speed, AsyncStorage for cross-launch durability
- Selective persistence — only high-value data is written to disk, not every query result
Retry logic
Writes (create/update/delete) never fail silently offline — they're pushed onto a persisted mutation queue with exponential backoff and jitter, so a batch of failed requests doesn't hammer the server the moment connectivity returns. Each queued mutation carries an idempotency key so a retry that technically succeeded server-side (but timed out client-side) doesn't create a duplicate.
Retries are capped (5 attempts) with a dead-letter state — after exhausting retries, the mutation surfaces to the user as 'needs attention' instead of retrying forever in the background.
- Exponential backoff with jitter: 1s → 2s → 4s → 8s → 16s, capped at 5 attempts
- Idempotency keys prevent duplicate writes on ambiguous timeout retries
- Failed-after-retry mutations surface in a 'needs attention' UI instead of retrying silently forever
Network hooks
A single useNetworkStatus hook (built on NetInfo) is the source of truth for connectivity, debounced by 500ms to avoid flapping on marginal signal. The mutation queue subscribes to this hook and only drains when connectivity is confirmed stable, not just 'briefly reachable.'
Screens consume a useOfflineQuery wrapper around React Query that automatically pauses refetch-on-reconnect storms — instead of every screen refetching the instant the network returns, reconnection triggers a single coordinated revalidation pass ordered by screen priority (visible screen first).
- Debounced connectivity detection avoids false 'back online' triggers on flaky signal
- Mutation queue only drains on confirmed stable connectivity, not first ping
- Coordinated revalidation on reconnect — no thundering-herd refetch across all screens at once
Results
The app remained fully navigable and mostly interactive during subway-style connectivity drops, with writes queued and reconciled automatically. Support tickets referencing 'lost' actions (form submits that silently failed offline) dropped to near zero after the mutation queue shipped.
- Zero silent write failures — every offline action is queued, retried, or explicitly surfaced
- Read screens stay interactive with cached data even with zero connectivity
- Reconnect no longer causes a visible 'refetch storm' or UI jank