Offline-first when drivers lose signal for half the day
Drivers lose signal for half the day, so the app has to hold a day of work on the device. What that costs, and the conflict nobody designs for up front.
There is a version of “offline support” that means the app shows a cached list and a spinner, and there is a version that means a driver can work a full shift through dead zones and nothing is lost. They are different products, and the gap between them is not caching. It is deciding, in advance, what happens when two people changed the same thing while one of them was unreachable.
We built dispatch and a driver app for a fleet of more than 200 vehicles where routes cross places with no coverage. Everything below comes from that.
Offline is not the edge case
The instinct is to build the connected path first and add offline handling later, as a degraded mode. That ordering is what produces apps that lose data, because the offline path ends up inheriting assumptions that only hold online — a server-assigned id, a validation that needs a lookup, a timestamp taken from the response.
The inversion that works: the device writes to local storage first, always, connection or not. Sync is a separate process that reconciles local state with the server whenever it can. The connected case is not a different path; it is the same path with a shorter queue.
The practical test is whether the driver can tell. In a system built this way they cannot, and that is the point — nobody should have to think about connectivity to record that a delivery happened.
The conflict nobody plans for
Here is the case that decides your architecture, and it is worth writing on a whiteboard before any code:
A driver is assigned route A at 08:00 and drives into a dead zone. At 10:00 the dispatcher reassigns them to route B, because a customer moved a slot. At 11:00 the driver, still offline, records two deliveries against route A. At 11:30 signal returns.
Nothing here is a mistake. Both people acted correctly on the information they had. Yet the server believes the driver is on route B and the device is about to report work on route A.
“Last write wins” is the default answer and it is wrong twice over. First, the device clock is not trustworthy — phones drift, get set manually, cross timezones — so “last” is not a fact you have. Second, and worse, the two changes are not competing versions of the same field. The dispatcher changed an assignment. The driver recorded events that happened. One of those is a plan and the other is history, and history does not lose.
Separate what happened from what was planned
That distinction turned out to be the load-bearing one.
Events are append-only and always win. A delivery that occurred, occurred. It gets an id generated on the device, it syncs, and it is never overwritten by the server — at most it is attached to a different parent.
Assignments are server-owned. The dispatcher is the authority on who should be where. A device does not get to overrule that; when it reconnects it takes the current plan.
The overlap is a reconciliation problem, not a conflict. Once events cannot conflict with plans by construction, the reconnect case stops being “who wins” and becomes “the driver did work under a plan that has since changed” — which is a normal operational situation with a normal operational answer: surface it to a human, do not resolve it silently.
That last part is the one teams skip. There will be a residue of cases the rules cannot decide, and the correct destination for those is a queue somebody looks at, not a heuristic. A system that silently picks a winner produces numbers nobody can explain three weeks later.
Ids and clocks
Two mechanical things cause most of the pain, and both are cheap to get right at the start and expensive later.
Generate ids on the device. A record created offline needs an identity before it has ever touched a server. UUIDs are fine. Waiting for a server-assigned id means the device holds records it cannot reference, and every relationship between them becomes a fixup during sync.
Send the device clock, but do not trust it. Record the device timestamp because it is what the driver saw, and record the server receipt time separately, because that is the one you can order by. Collapsing these into one field is a decision you cannot undo once the data exists.
What it costs
Offline-first is not free and it is worth being explicit about the bill.
Every screen has more states — pending, synced, failed, conflicted — and each needs a design decision rather than a spinner. Testing is harder, because the interesting bugs only appear in sequences: offline, edit, offline again, reconnect during an upload. Storage on the device needs managing, because “a day of work” includes photographs and signatures. And the sync layer is real software that needs its own retries, its own idempotency and its own observability.
For a fleet where drivers are offline half the day, that cost is not optional — anything else fails exactly where the work happens. For an app whose users sit in an office on wifi, it is substantial engineering bought for a problem they do not have. The question worth asking early is not “should this work offline” but “for how long, and doing what”, because a system that tolerates a two-minute lift ride and one that holds a full shift are not the same build.
The full case study has the rest of the constraints on that project, including the accounting system that was not going to be replaced. If you are looking at similar work, our custom software practice is where it lives.
Related
- When a spreadsheet becomes load-bearingA spreadsheet becomes a system the moment it starts holding rules rather than numbers. The observable signs, and the honest case for leaving it alone.
- Transport management systemDispatch, route planning and a driver app that keeps working without signal, for a logistics fleet of 200+ vehicles that had outgrown its spreadsheets.
- Declare maintenance jobs as data, not as codeA maintenance tool that can run any command is one nobody should trust with admin rights. What changes when the job is declarative data instead of code.
Does your version have a constraint this guide does not?
That constraint is usually the whole problem. Describe it and we will tell you whether it is the kind of thing we take on.
Start a projectWe reply within 1 business day. No sales calls unless you ask for one.