CodeCrafting All articles
Build & Delivery

The Bars Will Vanish: Designing UK Apps for a Network That Isn't There

CodeCrafting
The Bars Will Vanish: Designing UK Apps for a Network That Isn't There

Photo: Océanos y dados, CC0, via Wikimedia Commons

There is a particular kind of optimism in a London office at 10am. The fibre is fast, the Wi-Fi is quiet, and every request resolves before you've alt-tabbed back to the ticket. Then the release goes out and someone tries to use it on the Paddington to Penzance train, or in a village hall in Norfolk, or in a football crowd where 40,000 people are all trying to check the same score. The bars vanish. The spinner spins. The user sighs and closes the tab.

We talk about performance budgets and accessibility audits, and rightly so. But connectivity is still treated as a background assumption rather than a dependency with a temper. For a UK audience, that assumption is doing a lot of unpaid overtime. We have rural broadband that ranges from excellent to ornamental, train Wi-Fi that behaves like a rumour, and event congestion that can make a city centre feel like a dial-up modem. If your software only works on a good day, it doesn't work.

The office fibre lie

The problem isn't that developers are careless. It's that our default test environment is unusually kind. A fast connection hides a multitude of sins: chatty requests, blocking spinners, optimistic updates with no rollback, and error states that assume the user will simply try again. On a slow link, those sins become the product.

So the first move is to stop thinking of the network as a pipe. Treat it as a dependency that can be slow, flaky, absent, or — worst of all — successful in a way the client never hears about. That last one is the nasty case: the request lands, the server writes the record, and the response dies somewhere in the wet Welsh hills. The user sees a failure and taps submit again. Now you have two orders, two bookings, or two support tickets.

Design for the dropped reply, not just the dropped request

Most offline advice focuses on what happens when a request never leaves the device. The more interesting case is when it leaves, arrives, and the acknowledgement never comes back. That is where durable local queues and idempotency keys earn their keep.

A practical pattern is to give every user action a client-generated identifier before it goes anywhere. The server uses that identifier to make the write idempotent. The client stores the pending action in IndexedDB or similar durable storage, shows it as pending, and retries with the same identifier until it gets a definite answer. If the app is closed and reopened, the queue survives. If the user jumps on the Tube and loses signal, the queue waits. If the server already processed it, the retry is a no-op rather than a duplicate.

This is not glamorous work. It is the software equivalent of keeping a paper receipt in your pocket until the card machine confirms. But it is the difference between a resilient product and one that quietly loses people's work.

Optimistic updates need a receipt

Optimistic updates are lovely when they work. The interface responds instantly, the user feels fast, and nobody stares at a spinner. The trouble is that optimistic UI often lies by omission. It says 'Saved' when it means 'Queued, probably, unless the browser evicts this tab and the server never hears from us'.

Be specific. Use states like 'Saving…', 'Saved on this device' and 'Waiting to sync'. Show pending items in the list with a subtle marker. Let people tap into a failed item and copy their text out. If a background sync fails permanently, don't silently discard it — surface it and offer a retry or export. The user should never have to guess whether their work exists.

For conflicts, keep it boring. Last-write-wins is fine for a draft note; it is not fine for a rota or a stock count. If two devices edit the same record offline, you need a merge story or a clear 'this changed elsewhere' message. That story should be written before the feature ships, not discovered in a support ticket.

Timeouts and retry budgets that respect people

An infinite spinner is not a loading state. It is a hostage situation. Every network call needs a timeout, and every retry needs a budget. After a small number of attempts, stop and tell the user what happened. Offer a manual retry, a way to copy their input, or a route to a lower-bandwidth fallback.

Exponential backoff with jitter is the standard advice, and it is good advice. But the user-facing side matters just as much. Don't fire five retries in the background while showing a calm 'Saving…' message. If the app is struggling, say so. A short, honest message — 'We can't reach the server right now. Your changes are safe on this device.' — builds more trust than a polished lie.

Also budget the whole interaction, not just one request. A page that makes 14 API calls on load will feel broken on a slow train even if each call has a sensible timeout. Collapse requests, cache what you can, and render a useful shell while the rest catches up.

Offline states that don't gaslight

The classic offline screen is a sad cloud and a 'Retry' button. It is better than nothing, but it often ignores the most important question: what happened to the thing I was doing?

A good offline state answers three things quickly. What is unavailable? What is safe? What can I do next? If the user was writing a long message, keep the draft. If they were uploading a photo, keep the file locally and show it queued. If they were browsing, show cached content with a clear 'last updated' label. Don't pretend stale data is live, and don't block the entire app because one non-critical request failed.

Offline-first is not an all-or-nothing architecture. Most teams do not need a full local-first database. They need a few well-chosen paths that degrade gracefully: drafts, form submissions, reading lists, and anything a user might reasonably do on a train.

A repeatable test plan for patchy Britain

You can test this without driving to Cornwall. In Chrome DevTools, use the Network panel's throttling presets and add a custom profile for slow 3G with high latency. Reload mid-session, submit a form while throttled, and watch what the UI says. Then switch to flight mode, repeat the same actions, and close the tab before reconnecting. Reopen it and see whether the pending work survived.

Do a short 'train walkthrough' before each release: load the app, start a task, lose the connection, background the app, foreground it, reconnect, and check that the result is exactly one record and a clear status. Try a slow response that succeeds after the client has already timed out. Try a double-tap on submit. Try a server 503 followed by a success on retry. These are not exotic edge cases; they are Tuesday.

Keep a short offline chaos checklist in the release notes: kill the network during submit, refresh while a request is in flight, duplicate the action, force a timeout, fill local storage, and change the device clock. If the app survives those with its dignity intact, you're in good shape.

Ship for the country you have

The UK is not one network. It is a patchwork of fast city fibre, tired copper, shared train hotspots and crowded stadium cells. Building for that reality is not defeatism. It is good engineering: smaller payloads, clearer states, durable local work, and honest messages when the signal drops. Your users won't notice the idempotency keys. They will notice that their draft is still there when the train comes out of the tunnel.

All Articles

Related Articles

The Hour That Runs Twice: Testing Your Stack Against GMT and BST

The Hour That Runs Twice: Testing Your Stack Against GMT and BST

From Background Hiss to Clear Signal: A Warning Budget for Small Teams

From Background Hiss to Clear Signal: A Warning Budget for Small Teams

Guardrails, Not Gatekeepers: Putting Accessibility Checks in the Build

Guardrails, Not Gatekeepers: Putting Accessibility Checks in the Build