Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Offline should be the norm: building local-firs...

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers. →

Offline should be the norm: building local-first apps with CRDTs & Kotlin Multiplatform

In 2026, a loading spinner in the subway isn’t “bad connectivity”. It’s a cloud-first app showing its limits. This talk is about offline-first architecture: write locally first, and treat sync as a background process, not a user-blocking gate.

The hard part is concurrency: Alice edits a to-do offline while Bob checks it off. Who wins? Definitely not “last-write-wins”. We will unpack CRDTs, a data structure designed to merge changes without conflicts and converge reliably. Then we go hands-on with Kotlin Multiplatform by designing a practical local-first architecture, local persistence, a sync engine built around deltas, and the real-world challenges (tombstones, pruning, UI “glitches”). The goal: one robust, shared sync core for Android, iOS, the Web, and apps that stays trustworthy even when the network isn’t.

Avatar for Renaud MATHIEU

Renaud MATHIEU

October 21, 2026

More Decks by Renaud MATHIEU

Other Decks in Programming

Transcript

  1. DROIDCON LISBON 2026 Offline should be the norm Building local-first

    apps with CRDTs and Kotlin Multiplatform Renaud Mathieu - renaudmathieu.com Paris Android User Group - https://mobilemakers.fr/en/ 20 OCTOBRE 2026 · @RENAUDMATHIEU.COM
  2. IN THE MARKET, NO NETWORK What is displayed on the

    app? 1. Please connect to the internet and retry? 2. “Pastel de nata” or “Gluten free Pastel de nata”? 3. Are all the items checked or unchecked?
  3. PART 01 / 09 01 Principles of offline-first What it

    means for an app to work without a network.
  4. 01 · PRINCIPLES Why offline-first? Users encounter network lapses daily

    because of: Limited bandwidth Interruptions Infrequent access In areas with poor coverage. In elevators or tunnels. Wi-Fi-only tablets, rural areas, travel. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  5. 01 · PRINCIPLES What is an offline-first application? An offline-first

    app can operate without internet, with or without some features. Specifically, it must fulfill three criteria: 01 02 03 Remain usable without a reliable network connection. Present local data immediately, without waiting for a first network call. Retrieve data in a battery and dataconscious way (e.g. only sync on Wi-Fi or while charging). OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  6. 01 · PRINCIPLES The central role of the data layer

    All offline-first thinking starts in the data layer of the app architecture. This layer provides access to application data and business logic. The two fundamental operations on data are: 1. Reads 2. Writes Retrieve data to display to the user. Persist user input to retrieve later. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  7. 02 · DATA LAYER App architecture UI layer Domain layer

    (optional) Data layer OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  8. 02 · DATA LAYER App architecture UI layer Domain layer

    (optional) Data layer Repositories Data sources OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  9. 02 · DATA LAYER The two data sources An offline-first

    app has at minimum two data sources for each repository that uses network resources: LOCAL NETWORK The local data source The network data source OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  10. 02 · DATA LAYER The local source: the source of

    truth The local source is the source of truth (single source of truth). It must be the exclusive source of all data read by the upper layers. This guarantees data consistency across connection states. COMMON LOCAL PERSISTENCE Structured data Unstructured data Simple files Relational databases like Room. Protocol Buffers with DataStore. JSON, XML, etc. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  11. 02 · DATA LAYER The network source: the real state

    The network source represents the real state of the application on the server side. The local source is at best synchronized with it. It can be behind in both directions: Network ahead Local ahead The app must update itself when it comes back online. The network must be updated when connectivity returns. The domain and UI layers must never communicate directly with the network source. That is the responsibility of the repository. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  12. 02 · DATA LAYER · MODEL 1 OF 3, NETWORK

    Separate models per layer Each data source often needs its own representation. We therefore define three models: @Serializable data class NetworkAuthor( val id: String, val name: String, val imageUrl: String, val twitter: String, val mediumPage: String, val bio: String, ) OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  13. 02 · DATA LAYER · MODEL 2 OF 3, LOCAL

    Separate models per layer @Entity(tableName = "authors") data class AuthorEntity( @PrimaryKey val id: String, val name: String, @ColumnInfo(name = "image_url") val imageUrl: String, @ColumnInfo(defaultValue = "") val twitter: String, @ColumnInfo(name = "medium_page", defaultValue = "") val mediumPage: String, @ColumnInfo(defaultValue = "") val bio: String, ) OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  14. 02 · DATA LAYER · MODEL 3 OF 3, EXTERNAL

    Separate models per layer Each data source often needs its own representation. We therefore define three models: data class Author( val id: String, val name: String, val imageUrl: String, val twitter: String, val mediumPage: String, val bio: String, ) OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  15. Mapping functions Conversions between models are done via extension functions:

    Network → Local fun NetworkAuthor.asEntity() = AuthorEntity( id = id, name = name, imageUrl = imageUrl, twitter = twitter, mediumPage = mediumPage, bio = bio, ) Local → External fun AuthorEntity.asExternalModel() = Author( id = id, name = name, imageUrl = imageUrl, twitter = twitter, mediumPage = mediumPage, bio = bio, ) / OFFLINE-FIRST / / / 02 · DATA LAYER 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  16. 02 · DATA LAYER Why three models? This separation protects

    the external layers from minor changes in the local or network sources that do not fundamentally change the app's behavior. For example, a column rename in the database, or a change in the JSON format, does not affect the UI. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  17. PART 03 / 09 03 Synchronization Pull or push: two

    ways to catch up with the server.
  18. 03 · SYNCHRONIZATION Two synchronization models OFFLINE-FIRST 1 Basics When

    an offline-first app regains connectivity, it must reconcile its local data with the network's. This process is called synchronization. 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  19. 03 · SYNCHRONIZATION Pull-based synchronization The app contacts the network

    to read the latest data on demand. The common heuristic is navigationbased: the app only fetches data just before presenting it. Strengths Trade-offs • Relatively easy to implement • Prone to high data consumption (unnecessary re- • Unnecessary data is never fetched fetches) • Not well suited for relational data OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  20. 03 · SYNCHRONIZATION Push-based synchronization The local source attempts to

    replicate the network source as closely as possible. It proactively fetches an appropriate volume of data on first launch, then relies on server notifications to know when data is stale. Strengths Trade-offs • The app can stay offline indefinitely • Versioning for conflict resolution is non-trivial • Minimal data consumption (only fetches changes) • The network source must support synchronization • Works well for relational data OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  21. 04 · REACTIVE READS Reads via observable types The key

    pattern: read APIs expose Flow<T>. The repository reads directly from the local source and can only notify its consumers of changes by first updating the local source. class OfflineFirstTopicsRepository( private val topicDao: TopicDao, private val network: NiaNetworkDataSource, ) : TopicsRepository { override fun getTopicsStream(): Flow<List<Topic = topicDao .getTopicEntitiesStream() .map { it.map(TopicEntity asExternalModel) } } 2 Data > : : 1 Basics > OFFLINE-FIRST 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  22. 04 · REACTIVE READS Data flow Reads in an offline-first

    repository must read directly from the local source. Updates are written to the local source first, which then notifies its consumers because it is observable. 01 02 The ViewModel collects the Flow The repository exposes the from the repository. Flow from the local DAO. OFFLINE-FIRST 1 Basics 2 Data 03 04 When the network updates the local DB, the Flow emits automatically. The UI updates accordingly. 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  23. 04 · ERROR HANDLING Error handling: local source Read errors

    from the local source are rare but possible. We protect against them with the catch operator on Flow: class AuthorViewModel( authorsRepository: AuthorsRepository, ) : ViewModel() { private val authorId: String = private val authorStream: Flow<Author> = authorsRepository .getAuthorStream(id = authorId) .catch { emit(Author.empty()) } } OFFLINE-FIRST . . . 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  24. 04 · ERROR HANDLING Error handling: network source If errors

    occur when reading from the network, the app should use a retry heuristic. STRATEGY 1 Exponential backoff The app attempts to read from the network at increasing intervals until success or a stop condition is reached. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  25. 04 · ERROR HANDLING Error handling: network source If errors

    occur when reading from the network, the app should use a retry heuristic. STRATEGY 2 Network connectivity monitoring Read requests are queued until the app is certain it can connect. Once the connection is established, the queue is drained, data is read, and the local source is updated. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  26. 04 · ERROR HANDLING Error handling: network source Combining both

    strategies. In practice, both approaches are often combined: 01 02 Network monitoring to trigger synchronization at the right moment. Exponential backoff to handle temporary failures during synchronization. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  27. PART 05 / 09 05 Offline write strategies Online-only, queued

    or lazy, and the conflicts that follow.
  28. 05 · WRITES Offline write strategies If the recommended method

    for reading data is to use observable types, the equivalent for write APIs is to use asynchronous APIs, such as suspend functions in Kotlin. This avoids blocking the UI thread and simplifies error handling, since offline-first writes can fail when crossing a network boundary. interface UserDataRepository { suspend fun updateNewsResourceBookmark( newsResourceId: String, bookmarked: Boolean ) } OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  29. 05 · WRITES · STRATEGY 1 OF 3 Online-only writing

    The app attempts to write data via the network. On success, it updates the local source. On failure, it throws an exception. Use case Handling failure Transactions that must happen online and in near real time. Example: a bank transfer. • Hide the write UI if the app requires internet to write (disable buttons). • Popup message or transient prompt to inform the user they are offline. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  30. 05 · WRITES · STRATEGY 2 OF 3 Queued writes

    The object to write is inserted into a queue. The queue is drained with exponential backoff when the app comes back online. Use case: a good choice when • It is not essential that the data is written to the network. • The transaction is not time-sensitive. • It is not essential to inform the user of failure. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  31. 05 · WRITES · STRATEGY 3 OF 3 Lazy writes

    The app writes to the local source first, then queues the write to notify the network as soon as possible. This is the most complex strategy, as it can cause conflicts between local and network sources when the app comes back online. Use case Watch out Good choice when the data is critical for the app. Example: in an offline-first to-do list app, tasks added offline must be stored locally to avoid data loss. OFFLINE-FIRST 1 Basics 2 Data 3 Sync Writing data in offline-first apps often requires more thought than reading, due to potential conflicts. An offlinefirst app does not need to be able to write offline to be considered offline-first. 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  32. 05 · WRITES Conflict resolution OFFLINE-FIRST 1 Basics 2 Data

    If the app writes data locally when offline and that data is misaligned with the network source, a conflict arises. 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  33. 05 · CONFLICT RESOLUTION Last write wins This is the

    most common approach for mobile applications: 01 02 03 04 Each device attaches a timestamp to written data. The network source receives data from the different devices. It rejects data older than its current state. It accepts more recent data. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  34. PART 06 / 09 06 Mathematical foundations of CRDTs Why

    convergence is guaranteed by construction.
  35. CRDTs (Conflict-free Replicated Data Types) rest on precise algebraic properties

    that guarantee convergence by construction. 06 · FOUNDATIONS Mathematical foundations of CRDTs OFFLINE-FIRST Before writing any code, it is essential to understand the underlying mathematics. 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  36. 06 · FOUNDATIONS Partial order A partial order is a

    binary relation ≤ on a set S that satisfies three properties: PROPERTY DEFINITION Reflexivity ∀x ∈ S, x ≤ x Antisymmetry ∀x, y ∈ S, (x ≤ y ∧ y ≤ x) Transitivity ∀x, y, z ∈ S, (x ≤ y ∧ y ≤ z) x=y x≤z Why it ma ers. Phone A holds {Pastel de Nata}, phone B holds {Potatoes}. Neither contains the other, so neither is “newer”: they are concurrent, and a total order would force us to pick one. tt OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  37. 06 · FOUNDATIONS Join-semilattice A join-semilattice is a partially ordered

    set (S, ≤) in which every pair of elements x, y has a least upper bound (also called join), denoted x ⊔ y. Definition of the least upper bound x ⊔ y is the smallest element z such that: •x ≤ z •y ≤ z • ∀w, (x ≤ w ∧ y ≤ w) ⇒ z ≤ w OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  38. 06 · FOUNDATIONS Join-semilattice Properties of the join operation ⊔

    (merge). The operation ⊔ must be: Commutative Associative Idempotent x ⊔ y = y ⊔ x (x ⊔ y) ⊔ z = x ⊔ (y ⊔ z) x ⊔ x = x The order in which states are received does not matter. Receiving the same state twice changes nothing. You can merge in groups in any order. {Onions} ⊔ {Potatoes} = {Onions, Potatoes} OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  39. 06 · FOUNDATIONS Monotonicity of updates For the system to

    work, updates must be monotone: each update only “moves up” in the partial order. Formally, if s is the current state and f an update operation: s ≤ f(s) This means you can never go back. Information only grows. This is why elements like tombstones (deletion markers) are necessary: you do not “delete”, you add the information that an element was deleted. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  40. 06 · FOUNDATIONS Strong eventual consistency The central property of

    CRDTs is Strong Eventual Consistency (SEC). If: IF THEN Two replicas have received the same set of updates (in any order), They are in the same state. This is stronger than classic eventual consistency, because no manual conflict resolution is needed. Convergence is automatic and guaranteed by the algebraic structure. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  41. 06 · FOUNDATIONS The complete logical chain 01 02 03

    04 05 Partial order Semilattice Monotonicity Strong convergence Models incomparable concurrent states. Guarantees that a least upper bound (merge) always exists. Commutativity, associativity, idempotence Updates only “move up”. Same updates, same final state, guaranteed. OFFLINE-FIRST 1 Basics Invariance to reordering and duplication. 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  42. 07 · TAXONOMY · PRINCIPLE State-based CRDTs (CvRDTs) CvRDTs (“Convergent

    Replicated Data Types”) send their complete state to other replicas. The receiving replica merges the received state with its local state via the merge function. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  43. 07 · TAXONOMY · REQUIREMENTS State-based CRDTs (CvRDTs) 01 02

    03 The set of states must form a monotone join-semilattice. The merge function computes the least upper bound (LUB). Updates must be monotone (the state only grows). OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  44. 07 · TAXONOMY · PRINCIPLE Delta-state CRDTs Delta-CRDTs are an

    optimization of CvRDTs. Instead of sending the full state, only the recent changes (deltas) are sent. This combines the advantages of both approaches: FROM CVRDTS FROM CMRDTS Conceptual simplicity (semilattice, merge). Network efficiency (only transmit what changed). OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  45. 07 · TAXONOMY · HOW IT WORKS Delta-state CRDTs 01

    02 03 Each update produces a delta Deltas are accumulated in a (state fragment). buffer. OFFLINE-FIRST 1 Basics 2 Data 04 Periodically, the buffer is sent The receiver merges the delta to other replicas. with its local state via the same merge. 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  46. 07 · TAXONOMY Catalogue of common CRDTs G-Counter G-Set A

    grow-only counter. Each replica counts its own increments, and the total is the sum of all replicas. Use it for things like view counts or likes. A grow-only set: you can add elements, but never remove them. Perfect for append-only logs or event streams. OR-Set LWW-Register Add and remove, with add-wins on conflict. Last write wins. Use it for simple fields. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  47. 08 · IMPLEMENTATION Why Kotlin Multiplatform? The synchronization logic (CRDTs,

    merge, delta engine) is the most complex and critical part of the app. Duplicating it for each platform would be risky. KMP allows you to: Share Test once Isolate The sync engine between Android, iOS and the Web. The convergence logic, only once. The complexity inside a commonMain module. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  48. 08 · IMPLEMENTATION · SYNCHRONIZATION FLOW The delta sync engine

    ON THE DEVICE ON THE SERVER, THEN OTHER CLIENTS 1. The user makes a local modification. 1. The server merges the delta into its state. 2. The CRDT produces a delta (minimal state fragment). 2. The server broadcasts the deltas to other clients. 3. The delta is added to the pending delta buffer. 3. Clients receive and merge the remote deltas. 4. When the network is available, the buffer is sent to the server. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  49. 08 · REAL-WORLD CHALLENGES Tombstones PROBLEM SOLUTION To support deletion

    in an OR-Set, each deleted element leaves a tombstone. Over time, tombstones accumulate and consume memory and bandwidth. When we're sure all replicas have acknowledged a tombstone, it can be safely removed (pruning). OFFLINE-FIRST 1 Basics 2 Data Vector clocks help determine this: a tombstone is eligible for deletion if its timestamp is less than or equal to the local vector clock, indicating all replicas have processed it. 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  50. 08 · REAL-WORLD CHALLENGES UI glitches Problem. When a remote

    merge arrives, the UI may briefly display inconsistent intermediate states. For example, a deleted element may reappear for a fraction of a second before being removed. A B C Update batching DiffUtil / ListDiffer Optimistic UI Accumulate incoming deltas and apply them in a single merge before notifying the UI. Use diff algorithms to compute the minimal changes to apply to the UI, avoiding flickering. Show the expected result immediately and silently correct in the background if the merge differs. OFFLINE-FIRST 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  51. KEY TAKEAWAYS Write locally first, synchronize in the background. Convergence

    Deltas One codebase Known fixes CRDTs guarantee it by mathematical construction. Delta-state CRDTs are the best trade-off for mobile. KMP shares the critical logic across all platforms. Tombstones, pruning and UI glitches have known solutions. The result: a reliable app even when the network is not.
  52. 09 · CONCLUSION Two questions to keep OFFLINE-FIRST 1 Basics

    01 Why offline-first? 02 How do you resolve conflicts? 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap
  53. REFERENCES Thank you @renaudmathieu.com • Shapiro, Preguiça, Baquero, Zawirski, A

    comprehensive study of Convergent and Commutative Replicated Data Types (INRIA, 2011) • github.com/ljwagerfield/crdt • github.com/android/nowinandroid OFFLINE-FIRST · DONE 1 Basics 2 Data 3 Sync 4 Reads 5 Writes 6 Math 7 CRDTs 8 KMP 9 Wrap