People using their smart phones in the underground subway

Offline First Mobile Apps Pt 1: The Blueprint

A typical web-based desktop app will be rendered in a browser such as Google Chrome on a desktop or laptop. Every user request or screen will result in an associated REST call to the server, and the associated JSON will be retrieved and transformed into a screen update. For example, when a user loads a todo app, the server will issue a GET request to todo/list which will return a JSON array. If the user requests the todo detail another request to /todo/{id} will be issued.

A typical mobile app works very similar to a typical web app, where each request will in turn require one or more REST calls, and the subsequent REST response will be transformed into native views. Web applications are primarily bound by the limitations that the browser sandbox ecosystem places upon them. Native Android and iOS have much less restrictions and have the opportunity to support more complex, richer and featured applications.

Why?

When accessing webapps via laptops and desktops are connected to reliable Wifi and Ethernet connections. The user can assume a fast, low latency, reliable network with minimal compromises. The same cannot be said for mobile users. A mobile user’s experience may experience high latency, minimal or no coverage. One of the key advantages of native application experience over mobile web is the ability to intelligently support a seamless offline experience. Users can experience a compromised experience due to a variety of factors such as loss of server issues, poor signal or high latency. Every user has experienced the frustration of using mobile web app native which abruptly ceases to work or worse when they enter buildings or underground structures such as the subway system.

Another key differentiator between an offline first and online only app is performance and battery. To load a given screen the app will first display a loading indicator, queue a network request and wait for the completion response from server before parsing and updating the screen. The total time to complete the transaction will be the sum of the network request, server processing and response time. Requests that are dynamically generated on server may need to query additional services or databases. Between the request and update of the UI, the user may need to wait several seconds. An offline first application, however, can query a local datasource nearly instantaneously resulting in an optimal user experience. Optimizing the apps CPU, radio and screen usage can maximize battery life. Reducing, deferring, and coalescing network requests can also result in improved battery life of the application.

One of the best examples of an offline first experience is the Gmail application. The app will optimistically synchronize data both upstream and downstream. Regardless of the total number of emails stored in the user’s inbox, the network usage will be will bound to the size of the change from the last sync. If the connection fails during the synchronization the application will continue where it left off.

Apps that support a seamless experience when a user transitions between an online to offline state without compromising the experience are known as offline first applications. As the app ecosystem matures the cost of building and supporting a native app needs to do more than simply render content JSON to screens. Modern mobile consumers are accustomed to a growing number of high-quality apps that “just work”. Apps that are more reliable and respect the user’s battery and network offer a competitive advantage. Supporting offline first functionalities in mobile apps requires cooperation from both client and server and conscious decisions when designing and building the applications that will be discussed further. Many applications may use a combination of online-only, cache and offline first approaches where appropriate.

Offline First App syncs data locally

The discussion can be summarized by the following principles:

Offline first Principles

1. The network and/or server is not reliable: Reliable network, low latency and high availability servers are not the norm for a mobile experience. Offline first mobile apps assume the user is offline

2. Fetching network resources is slow: Fetching resources over the network such as a JSON resource will always be slower than fetching from a local source particularly if the resource is dynamically generated

3. Seamless Transition: The app may notify the user about the current network status unobtrusively but should not prevent them from completing their mission. When connection to server is re-established, the app should seamlessly detect the change and continue synchronizing without intervention

4. Queuing: All requests that require network access such as download requests and mutations should be queued and performed in background. Not all queued requests are equal. A request to see the latest data in an active session has higher priority than synchronizing supporting data such as config flags

5. Checking the network state alone is not enough: Online state can only be determined by successfully pinging a controlled server and receiving an expected response. Users may be proxied at public Wifi gateway, the server may be unavailable or there may be connection but high latency

6. Modern apps respect the user’s battery and network: The application should respect the user’s battery and network and only synchronize the data that has changed from the last synchronization, and only when notified of changes from server first. Low priority requests can be delayed and processed in a batch to avoid waking radio

Caching

Caching is a technique to retain the results of an expensive operation so subsequent requests of the same type can be served faster. Although caching is not a replacement for a complete offline only solution it is often a pragmatic first step before implementing a push based offline system. The caching layer can be implemented using a variety of algorithms such as Least Recently Used (LRU), First in First Out (FIFO) or other. Many applications may use a hybrid of online-only, cache and offline first approaches to support various use cases.

Publish/Subscribe Pattern

To support an offline first application mobile devices, need to efficiently synchronize large amounts of data from the server. The publish/subscribe or pubsub pattern is a form of asynchronous service to service communication commonly used in serverless and microservice architectures. The pub/sub architecture can be used to develop fault tolerant data replication such as synchronizing a server and mobile app database.

The pub sub pattern has several key advantages over the caching design described above:

- Only the deltas need to be synchronized. If the system contains 1 million records and 100 of them have changed since the last synchronization, synchronizing all data can be an expensive operation. Caching will fetch all data or nothing depending on the invalidation configuration

- Push based. When data is modified on server, it can send a push notification to all relevant subscribers via GCM or APNS avoiding pre-emptive network requests reducing network and battery

- Cache invalidation strategies can be complex and involve various tradeoffs that can result in excess network/CPU or not receiving timely updates

Publish Stream

The data on the server can be represented as a time series of transaction mutations. The primary data store will always contain the source of truth. Only the master data source can be mutated, all other nodes will be receive read only subsets from the primary node. Mutations will be one of create, update or delete operations. The mutations can modeled as a stream of immutable events or messages. The stream of mutation events will be known as the publish stream. Any point in time can be represented by replaying the events in order from the stream.

Example Publish Stream for a Todo app:

1. Add { id: 1, userId: 1, title: “Grocery Shopping”, isComplete: false }

2. Update { id: 1, userId: 1, title: “Grocery Shopping”, isComplete: true }

3. Add { id: 2, userId: 2, title: “Do Taxes”, isComplete: false }

4. Delete { id: 1 }

5. Add { id: 3, userId: 1, title: “Write Medium Article”, isComplete: false }

If the entire stream is replayed the final state will be:

1. { id: 2, userId: 2, title: “Do Taxes”, isComplete: false }

2. { id: 3, userId: 1, title: “Write Medium Article”, isComplete: false }

One important item to note is that the final dataset contains records from both user 1 and user 2. In a typical Todo app users will only want to subscribe to their data. The above data could thus be written to multiple different streams such as “My Todos”, or “My Incomplete Todos”. Designing and setting up publish streams will be covered in more detail in a future article.

Subscribe Stream

The primary node (server) will be responsible for publishing one or more streams of all mutations. The data can be replicated to the replicated nodes (mobile devices) by subscribing to the appropriate streams. The stream can be replayed and interrupted at any point and will always represent the system at a point in time. When a mobile device mutates data the server will perform the mutation on the primary node and write it the transactions to the appropriate streams. The client can subscribe to an event stream and receive the events asynchronously. As the events are received from the subscription the mobile app can persist the events to the local data store such as SQLite. Another key attribute is the stream can be interrupted and scheduled to resume at any point based on network, battery or when the application loses focus or device goes to sleep.

In the following example the Master node will synchronize various records from Node 1 (Master) to Node2 (Online Slave) and Node 3 (Offline Slave). As mutations are made in Node 1 they are immediately reflected into Node 2. The changes to Node 3 are queued and synchronized when a connection can be established.

Offline Mutations and Conflict Resolutions

When users wish to change data on the mobile device such as an add or update, the transaction must be queued. The transaction can be queued locally and sent to server when appropriate in the background. The local data cache can be mutated immediately to ensure user sees the latest data. If the data is shared and can be modified by multiple users such as a group the developer will need to determine how to resolve the conflict.

1. Last write wins: The last write to the system will overwrite any previous writes. If multiple users are writing to the same data they may be processed out of order. This strategy is useful if the data can be modified multiple times such as a text field

2. First write wins: The first write to the system will mutate the data. Subsequent writes can either ignore the transaction or return an error. This strategy is useful when making edits that can only occur once such as a status change

3. Merge: Subsequent writes to the data will intelligently modify the data so both requests are applied

Summary

Many applications may use a combination of online-only, cache and push based synchronization to achieve the optimal user experience. Push based synchronization primary use case occurs when the user can subscribe to a defined data set such as “My Todos”. Many applications may use a combination of offline sync, caching and online only to achieve the optimal user experience.

In this article, we investigated the various types of apps such as online only REST based, caching and pubsub based mobile applications. We set the design and groundwork to build an offline push-based app in a technology agnostic way. The next series of articles will go through building a simple Todo based application server and client and make specific technology choices.