Updating Apps On The Fly

Myntra decided to go app-only in early 2015. Though it was a business decision it had a large impact on how tech functions. In this article, I talk about how we update key portions of our native app instantly.

Problem

A major problem with apps is that the review process is costly, it takes at least a week for a fix to reach the end user. This demands stricter release cycles as the cost of making mistakes is high.

Building cross platform experiences using web views leads to a bad experience as they are not as performant as native views and the look and feel is alien.

And let’s also agree that the developer experience for building apps is not even close to the web.

But the bright side of us going app only was in terms of focus. We would now have time to solve these issues if we could figure out how to.

React native

At the same time Facebook released React Native (RN) and it was love at first sight. We were very comfortable with React and had been building cross platform layouts using the flexbox model for some time. RN’s hot reloading seemed like a big win for developer experience and productivity.

A key advantage was that the app runs from a javascript file (jsbundle) which can modified at any point of time.

Maybe we could bypass the app review process and build an eventually consistent native app (browser of some sort) with a simple updater which updates the jsbundle file that RN uses.

Implementation

Before building the updater we decided on the basic characteristics that the initial implementation will have:

It will use our internal npm registry for package management. We will initially use dist-tags for tagging versions of packages

It should eventually be able to support segmentation / releases of some sort

To start with each app would relate to a npm-package. The dist-tag stable for that package would represent the release version.

We came up with two API’s for the update system:

The version API :package/version which would tell the version of the stable dist-tag for the package

Version API

The bundle API :package/:version (where :version is not ‘version’) which would give the jsbundle of the npm package for the given version.

Bundle API

The initial jsbundle is bundled with the app. The app decides the bundle to use and whether it should update the jsbundle based on two values:

rn-updated which tells the app whether to use an updated jsbundle or the pre bundled jsbundle. Initially this is false which means that the bundled jsbundle is to be used.

rn-version which tells the app the version of the jsbundle that the app has to use. Initially set to the version bundled with the app. This is also used to locate an updated jsbundle.

These variables can be saved in any place where one saves any user data. I used NSUserDefaults for iOS and SharedPreferences for android.

The update process works as below:

On every app foreground event we would hit the version API.

If the version differs from the version on the app we will download the bundle from the bundle API and write it to the disk with the filename :package-:version and set rn_updated to be true and set rn_version to be the version downloaded.

The app decides which bundle to use as follows:

When the app wants to use a RN jsbundle it checks rn_updated, which if true means that there has been a bundle update.

It checks if the file for the version rn_version exists on disk. If the file exists it uses that file.

If bundle file does not exist it sets rn_updated as false and sets rn_version to be the packaged version and uses the bundled jsbundle.

This was fairly simple to implement. It took me a week to write the backend, a couple of weeks to write the client side implementation and a week to setup caching / servers etc.

At that time we were building the Fashion Feed on Android, it wasn’t slotted to be released on iOS any time soon, and hence was an easy choice for us to start off with.

The wins were immediate :

We were able to iterate quickly through weekly releases

Bug fixes pushed more frequently

Declarative code using React #BestIntroToFunctionalProgramming

Much better dev experience with hot reloading, i.e, no need to build the app for each small change

A few months later, a few open source solutions popped up - Microsoft’s codepush being one of them.

Rollouts

But as we went on using the updater, we realised that there was more room for awesomeness. The releases we made were binary in nature — we had to rollout to everyone or no one. It would be really useful if we had a way by which we could control rollouts.

Scalability was a major concern when we decide to segment. I soon realised calculating segments on the client side would simplify our problem.

We had an internal bucketing system that we were already using to power our AB tests. I made use of the same system to segment our users into buckets in order to do staged rollouts.

This enabled us to rollout our bundles to smaller sets of users

This also gave us the ability to segment our internal users, qa and the general customer base. We were now able to do controlled releases early on internally to dog food our bundles, before taking them to production.

The app now uses the new version API which would be of the form :package/:segment/version. Note that the urls are absolute — the url and response for any user will be the same.This makes them highly cacheable.

The response for the version API remains the same. The bundle API remains the same.

New version API

On the server side we would have a list of conditions for each package evaluated from top to bottom. The first release for a package should be the default condition. The default condition can be changed but never be removed and will always be at the bottom. Then we add conditions on top of this which get evaluated from top to bottom and falls down to the default version if all conditions fail. Conditions always have a version number associated with them.

Conditions are of two types:

Percentage release: we get to define percentage of users we want to release this to.eg, 10% — then we randomly generate 100 numbers less than or equal to 1000 (10 % of 1000). These people who fall into this segment will get the version as the condition version. Otherwise it moves to the next condition. In case we want to rollback we calculate a subset of the original numbers, if we want to increase the release percentage we add more numbers.

Bucket based conditions, where we specify a special segment of users. If they fall into the segment they get the specified version. Otherwise it moves to the next condition.

Rollouts Admin

While we were building this, RN for android came out and we decided to release this feature on both the platforms simultaneously.

Results

Over the last year our journey with RN has been beyond expectations. Some of our major wins with RN:

We now have weekly internal RN releases to catch bugs early

Most of our iOS app (>60 %) is now on RN and our Android app in on its way to doing so

The amount of reuse is overwhelming. Check out this screenshot of how we work on our apps simultaneously

Code sharing. Yay!

For more details on our React Native journey, check out Mario’s talk at DroidCon india :