·4 min read

Offline-First Architecture: Patterns and Pitfalls

Lessons learned from implementing offline-first in Inner Gallery and Coachy. Concrete patterns, conflict resolution and pitfalls to absolutely avoid.

TL;DR

Three patterns that make offline-first work: local source of truth, operation queue for sync, and deterministic conflict resolution. The main pitfall: underestimating bidirectional sync.

A photo app that requires a connection to display your own images? That's the reality of most cloud-first apps. With Inner Gallery and Coachy, I took the opposite path: the device is the source of truth.

But be warned: offline-first is not just "adding a cache." It's a complete architectural approach that rethinks the relationship between local and remote.

The 3 Pillars of Offline-First Architecture

1. The Local Database Is the Source of Truth

In a classic architecture, the server is king. In offline-first, your local database holds the truth. The server becomes one peer among others.

Concretely in Inner Gallery: when the user imports a photo, it's saved and encrypted locally immediately. No network wait, no spinner. The UI reflects the local state. If cloud sync is configured later, it happens in the background — the user never needs to know whether the network is working.

The principle is simple: write locally first, sync later. This radically changes the user experience. The app is always fast, always available.

2. Persistent Sync Queue

User actions generate events that are stored in a local queue. As soon as the network is available, the queue drains automatically.

The important thing: this queue must be persistent. If the app crashes or the user closes it, pending events must not disappear. In Coachy, I use a dedicated SQLite table with a status per event (pending, processing, done, failed) and a retry counter.

Each event has a retry with exponential backoff. First failure: 2 seconds. Second: 4. Third: 8. This avoids hammering a server that's having issues.

3. Deterministic Conflict Resolution

When two devices modify the same data offline, you need to settle it. I use three strategies depending on the context:

  • Last Writer Wins (LWW): the most recent timestamp wins. Simple, predictable. I use it for photo metadata (renaming, tags).
  • Union merge: for tag or category lists, I take the union of both sets. No data lost.
  • Manual resolution: for critical data (workout plan modified on two devices), I present both versions to the user. It's rare, but when it happens, it's better to ask than to guess.

Conflict resolution must be deterministic and predictable. The user should never wonder "why did my data change?"

The Pitfalls That Cost Me Time

Database Size Explosion

Inner Gallery stores photos locally. Without management, the database grows fast. My solution: an eviction policy based on access frequency. The least viewed photos (that aren't favorited) are the first candidates for freeing up space. Metadata always stays — only raw data is evicted.

Race Conditions on Sync

Two concurrent syncs modifying the same data create inconsistent states. This happened to me with Coachy when the app went to the background during a sync and kicked off a new sync upon returning to the foreground. The solution: a per-resource lock system. If a sync is in progress on an entity, the next one waits.

Unstable Mobile Connections

The mobile network isn't a binary on/off. There's "slow," "unstable," and "works one request out of three." A simple fixed retry isn't enough — you need to adapt behavior to the error type. Network timeout -> aggressive backoff. Server error -> moderate backoff. 401 response -> don't retry, invalidate the session.

Inner Gallery (full local): The simplest architecture. Zero network = zero sync problems. Perfect for private data. Limitation: sharing and automatic backup are harder to add after the fact.

Coachy (hybrid offline-first): More complex but more powerful. Sync enables sharing with a coach, automatic backup and analytics. The price: roughly 3x the complexity and harder debugging.

The choice depends on the use case. For sensitive and personal data, full local is unbeatable in simplicity and user trust. For collaborative apps, hybrid offline-first is the way.

Offline-first is a complete architectural philosophy. If you adopt it, go all the way: rethink your entire data layer. Half-measures create more problems than they solve.

Further Reading


Sources:

offline-firstmobilearchitecturesynclocal-first