Home/Blog/Offline-First Systems
Resilient Application Architecture

The 32 Percent Usage Gap: Building Offline-First Systems For Mobile Internet Gaps

GSMA reports a 32% mobile internet usage gap in Latin America: people live within mobile broadband coverage but do not use mobile internet. Software cannot solve affordability, skills, trust, or inequality alone, but it can stop treating perfect connectivity, unlimited data, and premium devices as defaults.

The usage gap is not the same as a coverage outage

The GSMA distinguishes the coverage gap from the usage gap. A covered person may still face an unaffordable device or data plan, limited digital skills, safety concerns, weak local content, shared hardware, or an unreliable experience. Offline-first architecture addresses only part of this problem: it reduces data transfer, wait time, failed work, and dependence on continuous sessions.

The design target is not “the app opens without internet.” It is that a user can understand available data, complete critical work locally, know what is pending, reconnect without duplicate side effects, resolve conflicts, and trust that the system will not silently lose an action.

Data budgetInitial install, route payload, image and media variants, background traffic, sync batch, and monthly usage.
Time budgetFirst useful screen, local write confirmation, maximum sync delay, retry deadline, and conflict resolution.
Storage budgetCached assets, local records, attachments, outbox, logs, encryption overhead, and eviction policy.
Trust budgetWhat can be shown or changed offline, token lifetime, local sensitivity, reauthentication, and audit evidence.

The local database should drive the interface

Android's offline-first architecture guidance recommends a local source as the canonical source of truth for reads. The same principle works for web applications using IndexedDB or another durable client database. UI components observe local state; network responses update local state; the screen does not oscillate between separate cache and server models.

Use the Cache API and service workers for versioned application assets and request strategies. Use IndexedDB or an embedded database for structured domain data, indexes, operation metadata, and attachments. A cached HTML shell is not a synchronized application, and localStorage is not a transactional database.

Represent writes as durable operations

When a user submits a form, captures an inspection, changes a task, or attaches evidence, commit the business record and an outbox operation in one local transaction. The UI can immediately show “saved on this device” while keeping “sent to the organization” as a separate state.

Each operation needs a stable ID, entity ID, actor, device, local sequence, base version, schema version, creation time, dependencies, payload reference, attempt count, next retry, expiry, and policy class. The server must accept an idempotency key or operation ID so replay cannot create duplicate orders, visits, tickets, or payments.

Offline synchronization state machine
01 Local readyRead without networkRender the latest local snapshot with freshness, permissions, storage status, and missing-data indicators.
02 Local commitSave action atomicallyWrite the domain change and outbox record together; show that the action is stored on the device.
03 QueuedWait for eligible syncRespect connectivity, battery, metered-network, attachment size, user choice, dependencies, and deadlines.
04 SendingPush an idempotent batchAuthenticate, send bounded operations, include base versions, and capture per-operation results.
05 AcceptedApply server acknowledgementStore canonical IDs, versions, timestamps, normalized fields, and remove only acknowledged outbox entries.
06 ConflictRun domain policyAuto-merge safe fields, preserve both revisions, ask the user, or reject a stale consequential action.
07 Retry or blockClassify the failureBack off transient errors; stop on auth, validation, quota, policy, expiry, or permanent business failure.
08 ReconciledPull incremental changesUse cursors and tombstones, update local state, surface outcomes, and retain minimal audit evidence.

Synchronization is a protocol, not a retry loop

Define push and pull semantics, ordering, pagination, cursors, version checks, deletions, attachment transfer, partial success, authentication expiry, schema migration, and protocol compatibility. A response should describe the result of every operation: accepted, duplicate, conflict, invalid, unauthorized, expired, rate-limited, or temporarily unavailable.

Pull changes incrementally from a stable cursor. Keep tombstones long enough for devices that reconnect after days or weeks. Compact logs only after the server knows which clients or retention windows matter. Never assume wall clocks are ordered across low-cost devices; use server versions, logical sequence, or causality metadata where correctness depends on ordering.

Conflict policy belongs to the domain model

Data typeUseful strategyExampleUnsafe shortcut
Append-only eventPreserve both events, deduplicate by operation ID, order by domain sequence where possible.Inspection evidence, field note, delivery scan, telemetry event.Overwriting the previous event because the new client timestamp is later.
Independent fieldsField-level merge with provenance and validation.Address note changed offline while another user changes phone preference.Last-write-wins for the whole record.
Collaborative text or collectionCRDT or operation-based merge when product semantics justify its complexity.Shared notes, checklist items, annotations.Adding a CRDT to inventory, money, or authorization without domain rules.
Finite inventory or capacityServer-authoritative reservation, conditional write, waitlist, or compensating workflow.Stock, appointment slot, seat, credit allocation.Accepting every offline decrement and allowing negative inventory.
Workflow transitionValidate expected state/version; reject or transform stale transitions explicitly.Closing a ticket already reassigned or cancelled.Blindly replaying state changes in original device order.
Payment or security actionRequire current authorization, online limits, idempotency, expiry, and server decision.Transfer, credential reset, access grant, high-risk approval.Queuing an unbounded sensitive action for automatic execution days later.

Retry only failures that can succeed later

Use timeouts, exponential backoff, jitter, retry caps, and a deadline. Honor server retry guidance. Avoid synchronized retry storms when thousands of devices reconnect after a regional outage. Batch operations within byte and count limits, but keep individual result handling.

Do not retry validation failures, revoked credentials, forbidden actions, expired operations, unsupported schemas, or unresolved conflicts. Expose these as blocked work with a clear repair path. “Still syncing” is not an acceptable permanent state.

Low-bandwidth UX starts with payload design

Set byte budgets for first load and common tasks. Ship only the shell and critical data, paginate history, use compact schemas, compress responses, request deltas, resize images before upload, separate thumbnails from originals, and make media downloads explicit on metered networks. Cancel superseded requests and avoid background refresh that provides no user value.

Progressive rendering should expose useful local content immediately. Every screen should distinguish fresh, stale, missing, locally modified, queued, syncing, conflicted, rejected, and synchronized data. Use specific text such as “3 inspections saved on this device; last server sync 2 days ago,” not a green dot that implies certainty.

Background sync is an optimization, not a guarantee

Service workers can cache assets and intercept requests. Background Sync may ask the browser to retry work when connectivity returns, but browser support and scheduling are not universal, and periodic sync remains experimental in some environments. The app must also sync on launch, resume, explicit user action, and ordinary foreground connectivity.

Native mobile jobs face similar battery, data, and operating-system constraints. Make the outbox durable; treat any scheduler as a trigger, not as the owner of the operation.

Offline authentication has strict limits

Cached authorization is a risk decision. Allow low-risk reads or drafts for a bounded period, but do not assume an old token permits a new high-impact action. Store the minimum sensitive data, encrypt using platform facilities, isolate users on shared devices, support remote revocation on the next connection, and clear local material on sign-out or managed-device wipe.

Require current online authorization for payments, privilege changes, credential recovery, destructive commands, or actions whose policy may have changed. If the device cannot verify current policy, preserve a draft rather than promising execution.

Test the state machine, not an airplane-mode screenshot

Test slow and lossy networks, DNS failure, captive portals, timeouts after the server committed, duplicate delivery, partial batches, reconnect storms, token expiry, clock skew, schema upgrades, deleted records, full storage, cache eviction, killed processes, app updates, shared devices, and conflicts after long absence.

Use deterministic model tests for merge and state transitions, property tests for operation ordering and duplication, integration tests against sync protocol versions, and device tests with network shaping. A happy-path offline demo proves very little.

Observe synchronization as a product workflow

Track local write latency, outbox depth and age, bytes uploaded and downloaded, sync success, retries, blocked operations, conflicts, duplicate acknowledgements, cursor lag, storage use, eviction, stale-data age, background-job execution, and user-initiated retries. Segment by app version, device class, country, carrier, connection type, and workflow.

The outcome metrics are completed tasks, recovered work, abandonment, support contacts, data cost, battery cost, and time from local action to organizational acknowledgement. A sync engine with 99.9% request success can still fail users if the remaining operations are the oldest and most important.

What I would build

I would build a reusable offline data layer with a local relational store, observable repositories, transactional outbox, versioned sync protocol, attachment manager, bounded retry scheduler, domain conflict handlers, encryption hooks, migration support, and an operations dashboard.

Product modules would declare policy: which records are readable offline, which writes can queue, maximum age, merge strategy, required online authorization, data and storage budgets, and user-facing states. This keeps offline behavior explicit instead of scattered across network-error handlers.

The design principle

Offline-first does not mean pretending the server does not exist. It means local work remains coherent when the network disappears, and synchronization is treated as a distributed-systems protocol with visible state, domain-aware conflicts, bounded risk, and evidence that no user action was silently lost.