Offline-First Architecture: How We Handle Data Sync for Field Teams

Stackademic

Offline-first architecture isn't a feature you bolt on. Here's how we built local-first storage, conflict resolution and sync queues for field teams working without signal.

Try building a mobile app for a warehouse basement, a mine site, or a stairwell in a half-finished building. Sooner or later you'll learn the lesson every field-service engineer eventually learns the hard way: connectivity is a nice-to-have, not a given. If your app falls over the moment the signal drops, you haven't really built a field tool. You've built a demo that only works in the office. Obvious once you say it out loud, sure, but it quietly wrecks a surprising number of otherwise well-built apps, mostly because we all learn to build for the happy path first and bolt offline support on afterwards. That almost never works as well as designing for it from day one, and we learned that the slow, expensive way.

This isn't a niche problem either, it's basically a rite of passage for anyone building field-service software. Mitti runs inspection and incident workflows for teams working in exactly these low-connectivity conditions: sites, warehouses, remote locations, so offline-first sync isn't a nice-to-have feature there; it's core infrastructure. Here's a practical walkthrough of how that kind of sync architecture actually gets built, where it tends to fall over, and the decisions that ended up mattering more than we expected going in.

Why "Just Cache It" Doesn't Cut It

The instinctive first pass at offline support usually goes something like: store what you can locally, sync when you're back online, job done. Fair enough for read-heavy apps, where a bit of staleness is barely noticeable. It falls apart fast the moment writes get involved, because now you've got a genuinely harder problem on your hands: what happens when two people edit the same record while both are offline, and their phones reconnect at different times?

That's really the crux of the whole thing. Offline-first architecture isn't about hiding the network from the user; it's about accepting that your data will temporarily diverge across devices, and having a sensible plan for stitching it back together. Get that wrong, and you end up with silently overwritten records, duplicate entries, or worse, the kind of bug where an inspection looks fine right up until someone notices it's quietly reverted to an earlier version.

The Core Building Blocks

Once you accept that divergence isn't some rare edge case but just how things work, the architecture tends to settle into a handful of building blocks. Nothing exotic, just a few pieces that each solve one part of the puzzle.

  1. Local-first storage. Every write hits a local database first, full stop, no matter what the connection's doing. From the user's point of view, the app shouldn't feel any different whether they've got full bars or none.
  2. A durable sync queue. Every local write also gets queued up as an outbound sync operation, stored somewhere persistent so a crashed app or a flat battery doesn't quietly lose it. Honestly, this queue is the backbone of the whole system.
  3. Conflict detection and resolution. When a queued write finally reaches the server and finds the record's moved on without it, something has to decide what happens next. Spoiler: "last write wins" is rarely the right call.
  4. Retry and backoff logic. Patchy connectivity means failed sync attempts are the norm, not some rare exception, so the queue needs sensible retry timing rather than hammering a flaky connection every second like a kid pressing a lift button.

Each one of those sounds simple enough in isolation. Conflict resolution is the one that ate most of our engineering time, so it's worth slowing down and actually going through it.

Worth saying too: you don't need to build all four in one heroic sprint. Local-first storage and the sync queue can ship first and already deliver most of the reliability people actually feel, because the app stops feeling broken the second the signal drops. Conflict resolution and finer backoff tuning can come later, once real usage tells you which conflicts genuinely happen, rather than the ones you dreamt up in a whiteboard session.

Handling Conflicts Without Losing Anyone's Work

Last write wins is tempting purely because it's easy to build, and it's almost always the wrong default for field data. If two inspectors both flag issues on the same asset while offline, whoever's phone happens to sync last shouldn't just quietly erase the other person's report. That's not really a conflict at all. It's two legitimate pieces of information that both deserve to survive.

What worked best for us was merging at the field level rather than overwriting whole records. Instead of treating an entire inspection as one atomic thing that either wins or loses outright, we tracked changes field by field, so two non-overlapping edits to the same record could merge automatically and nobody's input vanished. Genuine overlapping conflicts, where the exact same field got changed two different ways, still needed a human decision, but by that point they were rare enough to route to a simple manual resolution screen instead of something the system tried to guess at silently. That combination, quiet automatic merging for the common case and honest escalation for the rare one, did more for data integrity than any clever conflict algorithm we experimented with.

Making the Sync Queue Actually Trustworthy

A sync queue is only as good as its worst day, so it pays to design around failure rather than assume everything will go smoothly. Every queued operation needs an idempotency key, basically a guarantee that if a sync request accidentally gets sent twice, say a connection dropped mid-response and the client retried, the server doesn't process it twice and quietly spawn a duplicate.

Backoff logic matters more than you'd think. Retrying the second something instantly fails feels responsive, but it tends to hammer a weak connection at exactly the moment it's least able to cope, which makes things worse rather than better. Exponential backoff, waiting a little longer after each failed attempt, gives a shaky connection room to breathe, and in practice it produces noticeably better sync success rates than an aggressive retry-immediately approach, even though it looks less eager on paper.

Testing Somewhere That Isn't the Office Wi-Fi

Here's the lesson that's easy to nod along to and genuinely hard to act on: you cannot properly test offline-first behaviour on a fast, stable office connection. Most of the ugliest bugs we hit only turned up under real conditions: a connection dropping mid-sync rather than cleanly, a phone jumping to a completely different network mid-task, three separate sync attempts stacking up because someone kept tapping retry out of frustration.

Network condition simulation earns its keep here. Deliberately testing against throttled bandwidth, intermittent drops and high latency turns up problems a normal QA pass will never find, and it's worth treating as a standard part of the test suite rather than a box you tick right before launch. Almost none of the bugs that made it to production were ever caught on a clean connection.

One of the more useful things we did was rope in a handful of actual field workers as informal testers on their real sites, rather than leaving it entirely to engineers faking bad conditions from a desk. They found failure modes nobody on the team had thought to test for, like a phone losing connection mid-photo-upload and needing to resume that upload later instead of silently dropping it. That's the sort of bug a lab will never surface, because a lab doesn't have someone standing in a stairwell trying to finish an inspection before their shift ends.

What We'd Do Differently

A few things are obvious only with hindsight, as these things usually are. We'd build field-level conflict tracking in from the very first version rather than starting with record-level locking and migrating later, because that migration was genuinely painful and touched almost every corner of the sync layer. We'd also invest earlier in visibility. A simple dashboard showing queue depth and sync failure rates per device would have caught a slow-building backlog on one site weeks before we actually noticed it, which only happened when a supervisor mentioned, almost in passing, that "some inspections seem to be taking a while to show up."

The bigger, more transferable lesson is that offline-first isn't really a feature you bolt onto an app. It's a stance you take from the start, because retrofitting it later means touching your data model, your API contracts and your client state management all at once, and none of that is fun to unpick after the fact.

Conclusion

None of this needs exotic technology. Local databases, queues, conflict resolution- these are all well-understood problems on their own. What makes offline-first architecture genuinely hard is that it forces you to design for a state most apps pretend doesn't exist, one where the data is temporarily, unavoidably out of sync with itself. Accept that early, build your data model around field-level changes rather than whole records, and test on a properly bad connection instead of the office Wi-Fi, and most of the painful lessons in this piece stop being inevitable and start being avoidable.

If there's one habit that separates teams who get this right, it's not a technology choice at all. It's treating "offline" as the default state to design for, and "online" as the happy bonus on top, rather than the other way round. It's a small mental shift, but it quietly reshapes almost every decision downstream, from how the data model is built to how failures get surfaced to the person standing there holding the phone at the end of a long shift.

Comments

Loading comments…