Blog
Offline-First Mobile Apps: The Outbox Pattern
How to build a mobile app that works on a bad connection — a durable, prioritised outbox, choosing local storage, conflict resolution, and the failure cases that decide whether users trust it.
Users do not experience your app on your office wifi. They use it on a train, in a lift, on a hotel connection that resolves DNS but moves no data. If your app's answer to that is a spinner and a failed request, it feels broken — even though the network, not the app, is at fault.
Offline-first is the alternative: the app writes to local storage first, shows the result immediately, and syncs in the background when it can. Getting that right is mostly about what happens when the connection comes back.
Optimistic UI is not offline-first
A lot of apps described as offline-first are really just optimistic. They show the message as sent, fire the request, and quietly drop it if it fails — or worse, retry it forever in a loop that drains the battery and occasionally sends it twice.
Genuinely offline-first means the local database is the source of truth for the UI, every mutation is recorded durably before it is attempted, and sync is a separate process that reconciles local state with the server. The user sees local state. The network is an implementation detail.
The outbox pattern
The durable piece is an outbox: a local table of pending operations, each with a type, a payload, a status and a retry count. Instead of calling the API, the app writes to the outbox and returns. A worker drains it.
Why this beats retrying in place
- It survives a cold start. If the user force-quits mid-send, the operation is still in the database when they come back.
- It keeps ordering meaningful. A reply cannot sync before the message it replies to.
- It makes failures visible. A row with five failed attempts is a thing you can surface, retry, or let the user cancel — rather than a request that vanished.
Not all operations are equal
This is where a naive queue disappoints. A chat message the user just typed and a profile-picture upload are not equally urgent, and draining strictly in insertion order means the message waits behind a multi-megabyte image on a weak connection.
Give each operation a priority and drain the queue by priority first, order second. On Chattick — an encrypted messaging app where the entire product is the speed of a sent message — that priority-based outbox is what keeps sending responsive while bulk media syncs behind it.
Choosing local storage
The local store is the thing the UI reads on every frame, so it needs to be fast, queryable and capable of feeding reactive streams. In a Flutter app that usually means an embedded database rather than key-value preferences, which run out of road as soon as you need to query, sort or watch a collection.
Two properties matter more than raw benchmarks: whether it can stream query results into your UI so screens update automatically as sync writes land, and whether it holds up with a realistic dataset — years of messages, not the twenty rows in your test fixture.
The hard part: conflicts
Two devices edit while offline. One wins. Decide which, deliberately:
- Last write wins is simple and adequate for single-user data like settings or drafts, as long as your timestamps come from a source you trust.
- Server authority — the server rejects stale writes and the client reconciles — suits anything involving money, inventory or bookings, where silently overwriting is unacceptable.
- Append-only models dodge conflicts entirely. Messages, events and ledger entries are inserts, never edits, which is why chat is one of the easier things to sync correctly.
Whatever you choose, write it down. Undocumented conflict behaviour becomes a bug report that nobody can reproduce.
Details that decide whether users trust it
- Idempotency. Every queued operation needs a client-generated ID the server can deduplicate against. Without it, one flaky connection turns into two identical orders.
- Backoff. Retry with exponential backoff and a cap. Tight retry loops on a dead connection are a battery complaint waiting to happen.
- Honest status. Show sending, sent and failed states. Users forgive a delay they can see; they do not forgive a message that silently never arrived.
- Bounded growth. Decide what happens after a week offline. Cap the queue, expire stale operations, and never let the outbox grow without limit.
- Test the ugly cases. Airplane mode is the easy test. The instructive ones are a connection that accepts requests and never responds, a token that expires mid-sync, and a force-quit halfway through draining the queue.
When not to do this
Offline-first adds a local schema, a sync worker, conflict rules and a class of bugs that only appear on real devices. It is worth it for messaging, field tools, travel apps and anything used somewhere with poor coverage.
It is not worth it for a product that is meaningless without the network — live scores, a video call, a payment terminal. There, the right investment is honest connection states and fast recovery, not a sync engine.
If you are weighing it up for your own product, it is exactly the kind of architectural decision we settle during MVP discovery, before it becomes expensive to change.
Frequently asked questions
What is the outbox pattern in mobile apps?
Is offline-first worth the complexity?
How do I stop duplicate writes when syncing?
How should conflicts be resolved?
What should I test beyond airplane mode?
Ready to scope your MVP? Get in touch or explore our portfolio, or grab the free MVP scoping checklist.
Ready to build something remarkable?
Book a free 20-minute scoping call. We'll review your idea, share relevant case studies, and outline a realistic timeline.
Reply within 24 hours · No commitment required