·5 min read

Architecture offline-first : patterns et pièges

Retour d'expérience sur l'implémentation offline-first dans Inner Gallery et Coachy. Patterns concrets, résolution de conflits et pièges à éviter absolument.

TL;DR

Trois patterns qui font fonctionner l'offline-first : source de vérité locale, queue d'opérations pour la sync, et résolution de conflits déterministe. Le piège principal : sous-estimer la sync bidirectionnelle.

Une app photo qui nécessite une connexion pour afficher tes propres images ? C'est pourtant la réalité de la plupart des apps cloud-first. Avec Inner Gallery et Coachy, j'ai pris le chemin inverse : l'appareil est la source de vérité.

Mais attention : offline-first ne se résume pas à "ajouter un cache". C'est une approche architecturale complète qui repense la relation entre local et distant.

Les 3 piliers de l'architecture offline-first

1. La base locale est la source de vérité

Dans une architecture classique, le serveur est roi. Dans l'offline-first, c'est ta base locale qui détient la vérité. Le serveur devient un peer parmi d'autres.

Concrètement dans Inner Gallery : quand l'utilisateur importe une photo, elle est sauvegardée et chiffrée localement immédiatement. Pas d'attente réseau, pas de spinner. L'UI reflète l'état local. Si une sync cloud est configurée plus tard, elle se fait en arrière-plan — l'utilisateur n'a jamais besoin de savoir si le réseau fonctionne.

Le principe est simple : écrire localement d'abord, synchroniser ensuite. Ça change radicalement l'expérience utilisateur. L'app est toujours rapide, toujours disponible.

2. Queue de synchronisation persistante

Les actions de l'utilisateur génèrent des événements qui sont stockés dans une queue locale. Dès que le réseau est disponible, la queue se vide automatiquement.

Le truc important : cette queue doit être persistante. Si l'app crash ou que l'utilisateur la ferme, les événements en attente ne doivent pas disparaître. Dans Coachy, j'utilise une table SQLite dédiée avec un statut par événement (pending, processing, done, failed) et un compteur de tentatives.

Chaque événement a un retry avec backoff exponentiel. Premier échec : 2 secondes. Deuxième : 4. Troisième : 8. Ça évite de marteler un serveur qui a un problème.

3. Résolution de conflits déterministe

Quand deux appareils modifient la même donnée offline, il faut trancher. J'utilise trois stratégies selon le contexte :

  • Last Writer Wins (LWW) : le timestamp le plus récent gagne. Simple, prévisible. Je l'utilise pour les métadonnées de photos (renommage, tags).
  • Fusion par union : pour les listes de tags ou catégories, je fais l'union des deux sets. Aucune donnée perdue.
  • Résolution manuelle : pour les données critiques (plan d'entraînement modifié sur deux devices), je présente les deux versions à l'utilisateur. C'est rare mais quand ça arrive, il vaut mieux demander que de deviner.

La résolution de conflits doit être déterministe et prévisible. L'utilisateur ne doit jamais se demander "pourquoi mes données ont changé ?".

Les pièges qui m'ont coûté du temps

L'explosion de la taille de base

Inner Gallery stocke des photos en local. Sans gestion, la base grossit vite. Ma solution : une politique d'éviction basée sur la fréquence d'accès. Les photos les moins consultées (et non favorites) sont les premières candidates pour libérer de l'espace. Les métadonnées restent toujours — seules les données brutes sont évictées.

Les race conditions sur la sync

Deux sync concurrentes qui modifient les mêmes données créent des états incohérents. Ça m'est arrivé avec Coachy quand l'app passait en background pendant une sync et relançait une nouvelle sync au retour au premier plan. La solution : un système de locks par ressource. Si une sync est en cours sur une entité, la suivante attend.

Les connexions mobiles instables

Le réseau mobile n'est pas binaire on/off. Il y a le "lent", le "instable", le "ça marche une requête sur trois". Un simple retry fixe ne suffit pas — il faut adapter le comportement au type d'erreur. Timeout réseau → backoff agressif. Erreur serveur → backoff modéré. Réponse 401 → ne pas retenter, invalider la session.

Inner Gallery (full local) : l'architecture la plus simple. Zéro réseau = zéro problème de sync. Parfait pour les données privées. Limitation : le partage et le backup automatique sont plus complexes à ajouter après coup.

Coachy (hybrid offline-first) : plus complexe mais plus puissant. La sync permet le partage avec un coach, le backup automatique et les analytics. Le prix : une complexité environ 3x supérieure et un debug plus difficile.

Le choix dépend du use case. Pour des données sensibles et personnelles, le full local est imbattable en simplicité et en confiance utilisateur. Pour des apps collaboratives, l'hybrid offline-first est la voie.

Offline-first est une philosophie architecturale complète. Si tu l'adoptes, va jusqu'au bout : repense tout ton data layer. Les demi-mesures créent plus de problèmes qu'elles n'en résolvent.

À lire aussi


Sources :

offline-firstmobilearchitecturesynclocal-first