Designing for the network you actually have
Apps people keep still work on weak networks, and that is decided before design begins.
8 min read
The most common defect in mobile software is not a crash. It is an application that treats connectivity as a precondition. It works flawlessly in the office, in the demo and in the app store screenshots, and then fails in exactly the moments a person remembers: the lift on the way to a meeting, the basement car park, the site visit with one bar, the aircraft, the moment the network is technically connected but nothing is getting through.
What makes this expensive is that it is not a bug to be fixed later. Offline capability is a property of the data architecture. An application built around the assumption that a request will succeed cannot be made offline-tolerant by adding retry logic — the retry has nowhere to put the work. Adding it afterwards is close to a rewrite, which is why the decision belongs in week one, before the first screen.
The question is not whether, it is how much
Offline is not binary, and treating it as such produces either an over-engineered app or a fragile one. It is a product decision expressed as a list of what a person can do with no connection at all:
- Read what they have already seen — the weakest useful level, and enough for a great many apps.
- Read a defined working set that was fetched in advance — today's route, this week's appointments, the assigned inspections.
- Create and edit, queued for later — the level at which field applications become viable.
- Operate indefinitely with periodic synchronisation — the level at which conflict resolution becomes a real design problem.
Each level costs meaningfully more than the one above it, and most products need less than their team assumes. A consumer app frequently needs only the first two. A driver, inspector or technician application needs the third and is worthless without it. Naming the level explicitly is what keeps the build proportionate.
Name the level in week one. It is the cheapest sentence in the project, and the only one that decides whether the architecture can support the answer at all.
Three things that are always harder than they look
Identity of things that do not exist yet
A record created offline needs an identity before any server has seen it. If identifiers are assigned by the server, the local record has no stable reference — so anything pointing at it breaks when the real identifier arrives. Generating identifiers on the device removes an entire class of defect and costs nothing, but it has to be decided at the schema, not patched in later.
Two people, one record
Once edits can happen offline, two people can change the same thing before either syncs. There is no universally correct resolution, only choices with different consequences: last write wins is simple and silently destroys work; field-level merging is better and only works if the model is granular; queueing intentions rather than states preserves both edits and complicates everything else; asking a person is honest and is only tolerable if it happens rarely.
The important part is choosing deliberately per entity rather than globally. A free-text note and a stock quantity have genuinely different correct answers, and a system with one policy for both will be wrong about one of them.
Time, which is not reliable
Device clocks are wrong, sometimes by a lot, and users change them. Any resolution rule that depends on comparing device timestamps will occasionally do something inexplicable. Ordering has to come from something more robust — a server-assigned sequence, or a scheme that tracks causality rather than wall-clock time.
Making the state visible without making it noisy
An offline-capable app has states a connected app does not: this is saved on your device, this is queued, this failed and needs you, this is now confirmed. Hiding those states produces an app people do not trust, because they have no way to know whether their work survived. Showing all of them constantly produces an app that feels broken.
The workable middle is quiet by default and specific when it matters. A subtle, persistent indicator of overall state rather than a banner per record. Explicit confirmation only where the stakes justify it. And when something genuinely cannot be resolved, an interface that says what the conflict is and lets a person choose — rather than an error code and a suggestion to try again.
Testing where it actually breaks
Connectivity has more than two states, and the interesting failures live between them. An interface that toggles between online and offline in testing will not have exercised the case that produces most real complaints: the connection that is technically present and delivering nothing.
Worth testing deliberately: high latency rather than no connection; requests that succeed on the server but whose response never arrives, so the client retries something that already happened; the transition mid-operation; a queue that has accumulated for days rather than minutes; and the captive portal that answers every request with a login page, which a naive client will happily parse as data.
The last of these is why idempotency belongs in this article as much as in a payments one. If an operation can be safely repeated, an unclear network outcome is no longer a decision the client has to get right — it just retries. Most of the difficulty in offline systems comes from operations that cannot be repeated safely, and a good deal of that difficulty is avoidable by design.
The device you are actually building for
One last calibration. In many of the markets where this matters most, the primary device is a mid-range phone that is several years old, frequently shared, on a metered connection. Payload size is not a refinement there; it is an adoption question. An application tested only on current flagship hardware over office wifi has not been tested against its users at all.
Facing a similar challenge?
Tell us where you are today. We will explain what the work involves and whether you need us at all.