Front-End Web & Mobile

AWS AppSync offline reference architecture – powered by the Amplify DataStore

This article was written by Fernando Dingler, Senior Solutions Architect, AWS

 

Modern mobile and web applications are built to provide delightful and seamless user experiences. As users we have become more demanding and we expect a great user experience for the mobile apps we interact with on a daily basis. For example, if we tap the Like button on a social media app, but the internet connectivity happens to be lost in that particular moment, we expect the application to take care of retrying and make it seem like the connection was never lost. In order to deliver that kind of user experience, frontend developers traditionally had to implement complex mechanisms for storing data in a local cache, resuming connections automatically, doing retries with exponential backoff, sync the data to the cloud and merge it with safe conflict resolution. It is not easy to implement such an experience, and we heard from developers that they would rather use their time working on new features as opposed to spending time on this sort of undifferentiated heavy lifting over and over.

AWS AppSync is a GraphQL serverless backend for mobile and web applications. It provides a flexible and reliable API with a built-in compute layer to run queries, mutations, and subscriptions to securely access, manipulate, and combine data from one or more data sources for any application. On the client device, the Amplify DataStore provides a familiar programming model for leveraging shared and distributed data without writing additional code for offline, real-time and online scenarios, which makes working with distributed, cross-user data just as simple as working with local-only data.

Today we share a reference application and architecture that demonstrates how to build a modern mobile application with built-in offline and cloud synchronization capabilities using React Native with the Amplify DataStore in the front end and AWS AppSync with Amazon DynamoDB in the backend.

 

Reference architecture

Amplify DataStore is a client that runs inside the mobile device or web browser and exposes an API for developers to interact with. It serves as a persistent repository to store data locally and synchronize it to the cloud automatically in the background via GraphQL queries, mutations, and subscriptions. Any interaction with the DataStore client is always against local data, while the synchronization, conflict detection, versioning, and journaling is handled automatically in the background by the Sync Engine and AWS AppSync. It’s worth mentioning that data synchronization to the cloud is an optional feature of DataStore, you can use it in a local-only standalone mode without an AWS account if needed.

Another important highlight is that developers don’t need to be familiar with GraphQL libraries like Apollo or Relay. They just need to invoke the DataStore APIs with standard function invocations and the client automatically transforms those operations into GraphQL queries, mutations, and subscriptions behind the scenes to interact with an AWS AppSync API endpoint. The following APIs are available in the DataStore to easily manipulate your data:

  • Save – create or update items.
  • Query – fetch items by ID or by using predicate filters. Supports pagination.
  • Delete – delete an item by ID or by using predicate filters.
  • Observe – subscribe a function to listen for changes in your models using AppSync real-time features.
  • Clear – delete all data from the local store, useful when signing out a user to clear local data.

Using the model generation tool from the Amplify CLI, developers start by defining their data model as a standard GraphQL schema, the CLI automatically generates the models in native code and also creates the GraphQL statements (queries, mutations, and subscriptions) so that you don’t have to hand code them. Moreover, by using the push command in the Amplify CLI, developers can automatically provision an AppSync API with DynamoDB tables and a set of GraphQL resolvers to perform CRUD operations over the data based on the defined schema. All of this without writing a single line of backend code.

 

A sample application

We decided to build a modern serverless Point of Sale (POS) implementation as a mobile application that showcases all the services described above. What better use case for offline capabilities than a critical business application that processes sales and requires to continue working even if there are small internet outages throughout the day? Store associates should continue processing transactions and serving customers without worrying about poor internet connectivity.

We chose a coffee shop theme for the Point of Sale app, so let’s walk through the user experience of a barista using the mobile application. It all starts with the home screen where the barista can add products to the cart when taking the customer’s order:

When the customer is ready to pay, the barista navigates to the checkout screen where the order summary is displayed along with the line items, quantities and taxes. Pressing the Checkout button saves the order using the DataStore and take you back to the home screen.


Finally, the user can browse the list of orders grouped by date. This is where subscribing to real-time changes to the data becomes very useful because this list populates dynamically as soon as new orders are added by any user of the Point of Sale application. There is no need to manually refresh the list, the DataStore observes and retrieves asynchronously new orders and automatically updates the app local storage causing the list of orders to always be up to date.


The application provides a smooth user experience as adding products to the shopping cart is fast and responsive, there is no lag in the user interface even if the network is unavailable. This is possible because the UI is interacting with the Amplify DataStore, which is always accessing data locally and synchronizing changes to the cloud in the background whenever there’s network connectivity. Granted this is a simple application with no complex computation requirements, but the key highlight is how simple it is to build a delightful responsive user experience with reliable and efficient data handling by using the Amplify DataStore.

 

Let’s look at some code

It all starts with the GraphQL schema definition. Developers define their application data model in a graphql.schema file and Amplify generates all the necessary constructs for the local data store in native language. Let’s take a look at the schema for the Point of Sale application:

type Order @model {
  id: ID!
  total: Float
  subtotal: Float
  tax: Float
  createdAt: String!
  lineItems: [LineItem] @connection(name: "OrderLineItems")
}

type LineItem @model {
  id: ID!
  qty: Int
  order: Order @connection(name: "OrderLineItems")
  product: Product @connection
  description: String
  price: Float
  total: Float
}

type Product @model {
  id: ID
  sku: String
  name: String
  price: Float
  image: String
}

Notice the @model and @connection directives, these are Amplify abstractions that allow developers to quickly create the backend for the application including DynamoDB tables and GraphQL resolvers to manipulate your data. The @connection directive in particular is used to define relationships between your models, such as “has one”, “has many” and “belongs to”. In this case, our model defines that an Order has many LineItems, and a LineItem belongs to an Order and a Product.

Let’s take a look at the Checkout function:

async function submitOrder(order) {
    const now = new Date().toISOString();
    // Save order header
    const newOrder = await DataStore.save(
        new Order({
            total: order.total,
            subtotal: order.subtotal,
            tax: order.tax,
            createdAt: now,
        })
    );

    // Save each lineItem
    const promises = order.lineItems.map(lineItem => {
        return DataStore.save(
            new LineItem({
                qty: lineItem.qty,
                description: lineItem.description,
                price: lineItem.price,
                total: lineItem.total,
                order: newOrder, // associate to order
                product: lineItem.product, // associate to product
            })
        );
    });

    await Promise.all(promises);
}

How simple is that? Just imagine what the code would look like if we didn’t use the Amplify DataStore, and had to implement error handling logic for internet unavailability as well as save data locally for later synchronization to the cloud. It would be a lot more complex than the code above.

 

Conflict resolution

When you have multiple clients sending concurrent transactions to the backend, there is always the possibility that clients attempt to change the same item at the same time. This is where conflict resolution comes in handy and it is another AppSync feature developers get out of the box with the DataStore.

Let’s review a more concrete example of where conflict resolution would be useful in the point of sales application: imagine two different baristas are trying to update the product catalog to change the properties of the products. One of them is updating the pricing of products and the other one is updating the product image. And as they do it, they both suddenly happen to update the same product at the same time:

AppSync resolves the conflict gracefully by allowing both updates to succeed. This strategy is called Automerge and it is the default when conflict resolution is enabled in a DynamoDB data source. It essentially uses the GraphQL type and field information to inspect the update and compare it to the current item that has been written to the table.

You can change this strategy to apply version checks to the entire object with Optimistic Concurrency where the latest written item to your database uses a version check against the incoming record. Alternatively, for developers who want to get even more control over the conflict resolution strategy, you can use a Lambda function where you define any custom business logic for conflicts when merging or rejecting updates.

To learn more about this feature, refer to the official documentation.

 

The Delta Sync table

When an AppSync mutation changes an item that is versioned, a record of that change is stored in a Delta DynamoDB table that is optimized for incremental updates. The purpose of this table is to allow clients to hydrate their local storage with results from one base query that might have too many records, and then receive only the data altered since their last query (the delta updates). The Delta Sync table is visible in your AWS account and updated automatically by the Amplify DataStore and AppSync resolvers, there is nothing developers need to do to populate it.

What does it mean for the Point of Sale application? The first time a user opens the application, the DataStore fetches Orders by doing a query to AppSync without a lastSync value specified and therefore the GraphQL resolver retrieves the data from the main Orders table.

The DataStore automatically tracks and includes a lastSync value for subsequent calls to fetch orders from the backend. This is called a delta query and it only fetches the data that has changed since the last synchronization from that client using the Delta Sync table. It is more efficient than going to the main table because the Delta Sync table is purposely designed for queries based on timestamps.

The Delta Sync table provides a journal that is kept up to date by the AppSync resolvers on every mutation. Items on the table are evicted periodically using the DynamoDB TTL attribute to prevent this table to accumulate stale data. This allows clients to fetch data efficiently as they switch from offline to online states. “Soft deletes” are another interesting feature the AppSync’s Delta Sync implementation with DynamoDB data sources offer, which means deleted items are kept in the Base table with a tombstone flag for a configurable amount of time. This gives developers the opportunity to implement an “Undelete” feature to handle accidental deletions by removing the BaseTableTTL attribute before the item is deleted permanently.

To learn more about how the DeltaSync table works, visit the official documentation.

 

Conclusion

With AWS AppSync and Amplify DataStore, frontend developers can build modern applications providing a delightful, fast and responsive user experience by just writing few lines of code using a familiar and simple programming model. AppSync powers your backend by providing a managed GraphQL API endpoint storing data in highly scalable DynamoDB tables while the Amplify DataStore powers your client application by providing robust data handling and a resourceful synchronization engine.

Head over to the GitHub repository to explore the Point of Sale application code we built and use it as an example to start your own React Native mobile application: https://github.com/aws-samples/aws-appsync-refarch-offline. You can try it yourself and deploy the mobile front end as well as all the backend services in your AWS account in minutes.

We can’t wait to see what you will build next with AWS AppSync and the Amplify DataStore!