Rewriting our Core Backbone App in Ember While Developing at Full Speed

Rewriting mid air

Our nearly 6 year old Backbone application is showing its age — but how can we change frameworks without stopping the world? In this article we’ll talk about how we’re doing just that: replacing the old Backbone app with a new shiny Ember app while we develop new features and keep everything running so we can continue to treat patients every day.

At Iora Health our business is primary care and our care teams use “Chirp”, the electronic medical records platform we’ve built, while seeing patients all day. We’re also a modern engineering team and are continuously making Chirp better. Chirp is designed to be enjoyable for our care teams and single page apps have helped us immensely. Unfortunately it’s been tougher and slower to make changes. Incremental refactors within Backbone weren’t going to be enough.

Our Ground Rules

We’ve all been a part of big bang rewrites and know those are never fun (and rarely successful). We gave ourselves some ground rules:

  1. Continue developing new features during the rewrite
  2. Release the changes incrementally as we go — no big bang release
  3. End up with an application architecture similar to what we would have come up with if we had started it as a greenfield project

Our Mental Model

It’s been helpful for us to think of this super simplified model of how single page apps work.

Super Simplified SPA Architecture

Super Simplified SPA Architecture

This is overly simplistic but it has helped us think about how we can swap out the framework one step at a time. Also, we’re using it to make sure we’re moving towards an end state that isn’t too far off from where a greenfield Ember project would end up.

Past Attempts

Using our simplified model, we can describe our existing Backbone application as below.

Architecture of current Chirp Application using Backbone

Architecture of current Chirp Application using Backbone

Backbone.js was truly innovative and absolutely the right tool for us to use in 2011. At the time, the alternatives were no frontend code or creating a spaghetti mess of jQuery — we’re tremendously grateful to Jeremy Ashkenas (@jashkenas) for creating it. Over the years we wound up building our own custom framework on top of Backbone and realized that wasn’t where we should be spending our time. We’ve experimented with a few approaches to bringing our app up to date before this current effort.

Backbone Marionette — We replaced our ad-hoc framework with Backbone Marionette. This worked but the effort stalled out with a portion of Chirp rewritten and we never had the appetite to tackle the rest of the application. We probably abandoned the effort because we felt the JavaScript community had moved beyond Backbone.

React Components — We tried working our way up from the bottom by writing our views as React components hosted inside of a Backbone view within the existing application. This worked well but only addressed the view layer. Even though we were happier to write React components, we never quite managed to eliminate the Backbone models or router, so this effort stalled out too.

The Ember Rewrite

With Ember we decided to approach the rewrite from the top-down: replacing the router first, then the models and leaving the views for last.

In order to be incremental and ship to production early and often, we’ve broken our effort into 3 phases:

  1. Create an Ember app that’s responsible for routing while leaving the Backbone models and views in place.
  2. Replace Backbone models with Ember Data models one route at a time, hydrating the existing Backbone models to pass into the existing views.
  3. Rewrite one view at a time using Handlebars and eliminating all Backbone from that view.

Ember Phase I

At the end of Phase I our architecture will look like this:

Phase I Architecture using Ember Router with Backbone Models and Views

Phase I Architecture using Ember Router with Backbone Models and Views

We’ve got an Ember application, Ember router and Ember routes playing nicely with the Backbone models and Backbone views.

Let’s get into the details and show some code! We’ll look at a page that shows a patient’s lab results for lab. When a patient’s blood work comes back from the lab we get the results from our external partners in a scary HL7 format out of the 1980’s and massage it into a reasonable JSON format. We then create a page with the patient’s lab results. The URL looks like /facesheet/123456789/labs/12 looking at lab 12 for patient 123456789.

Creating an Ember App

The first step was to create a new Ember-CLI app. This made us feel like we were off to a good start since we were using the same tool as any greenfield app. Ember-CLI does so much for us! The only tricky part was making sure the Backbone objects we’d want to use during this transition were available from Ember. The problem was solved by adding a few lines to our ember-cli-build.js to include the files from our legacy Backbone app in the Ember build.

// ember-cli-build.js
[
  'javascripts/vendor.js',
  'javascripts/application.js',
  'javascripts/templates.js',
  'stylesheets/application.css'
].forEach((file) => {
  app.import(`../public/assets/${file}`, { type: 'test' });
});

Creating Ember Routes to Replace our Backbone Routes

We recreated our Backbone routes using the Ember router. Luckily our existing Backbone routes fit pretty smoothly into the Ember nested route pattern so this was mostly a syntax change.

From the Backbone routing

routes: {
  ":patient_guid": "facesheet",
  ":patient_guid/labs": "lab",
  ":patient_guid/labs/:labId": "lab"
}

To the Ember routing

// router.js
AppRouter.map(function() {
  this.route('facesheet', { path: ':patientGuid' }, function() {
    this.route('labs', function() {
      this.route('show', { path: '/:labId' }, function() {
     });
    });
  });
});

Loading Backbone Models in an Ember Route

In order to do our rewrite incrementally and ship quickly, we needed to load our Backbone models inside our Ember routes. Luckily, the Ember model hook isn’t tied to Ember Data. It doesn’t care what it loads and waits for any returned promise to resolve before continuing on. Backbone models use promises so we can ask these legacy models to load their data and proceed. This is still a regular Ember route.

// routes/facesheet/lab/show.js
export default Ember.Route.extend({
  async model({ labId }) {
    const lab = new Iora.Models.Lab({ id: labId });
    await lab.fetch();
    return lab;
  }
});

In this example, we load the Backbone model from a global Iora.Models.Lab with the param labId. We wrote Chirp before import was widely available, so we stored our objects in globals and referred to them that way.

You may be wondering where the .then’s are in the code above. We’ve recently discovered the magic of async and await, which lets us treat promises synchronously and make everything much simpler to read. In this example, the async before the model hook means it is a function that returns a promise and the await lab.fetch() inside the function calls a promise, but synchronously waits for the promise to resolve before moving on to the next line return lab;. Someday this will be supported by browsers but for now it exists as a Babel polyfill. It’s kinda fun to view the transpiled source and see the crazy code the polyfill produces to make asynchronous promises appear synchronous or you can take it on faith that it works ¯\(ツ).

Handlebars Template

Our Handlebars template for this route is pretty simple. We load a component, passing along the Backbone model we loaded in the route.

// templates/facesheet/lab/show.hbs
<section class="facesheet-content">
  {{lab-show lab=model}}
</section>

On to the lab-show component.

Creating an Ember Component that Renders a Backbone View

This is another place where we integrate Ember with Backbone in order to more quickly get to production. The lab-show component doesn’t render itself the Ember way. In fact, it’s Handlebars template is empty. Instead, it renders a Backbone view into the area of the DOM Ember gives it using this.$() — similar to how components that use jquery widgets work.

// components/lab-show.js
export default Ember.Component.extend({
  didRender() {
    this._super(...arguments);
    const backboneView = new Iora.Views.Labs.Show({
      lab: this.get('lab')
    });
    this.set('_backboneView', backboneView);
    backboneView.setElement(this.$());
    backboneView.render();
  },
  willDestroyElement() {
    this._super(...arguments);
    const backboneView = this.get('_backboneView');
    if (backboneView) {
      backboneView.leave();
      this.set('_backboneView', null);
    }
  }
});

After Ember updates the DOM using the Handlebars template it calls didRender, so that’s where we need to intervene. We create our Iora.Views.Labs.Show Backbone view while passing in the lab model instance and saving it in a property for cleanup. Then, we tell the view it lives on this component’s portion of the DOM. Finally, we ask the view to render itself.

To avoid memory leaks, we need to do some cleanup when the component is destroyed. Using the willDestroyElement hook, we retrieve the previously-saved Backbone view and call the cleanup hooks on it, assuming they exist to remove the view’s associated event bindings and such.

Will It Blend?

Will it blend?

So far we have created an Ember application, added some routes, loaded our Backbone models and created an Ember component that renders our Backbone view. Are we there yet?

We can visit /facesheet/123456789/labs/12 and see our lab page — hooray! However, our care teams don’t just read the pages, they interact with them and clicking links doesn’t work — boo!

Not to worry! We’ll get over this final hurdle with one last step.

Dealing with Legacy Hash-Based URLs

When we created the Backbone app, pushState was not yet widely supported so we used hash-based URLs. The links inside of the Backbone views are created using the hash-based approach, which are broken in our bright new pushState-based Ember world. When we click a link to lab 13 our URL now looks like /facesheet/123456789/labs/12#facesheet/123456789/labs/12 and that’s not right!

We don’t want to open up our Backbone code and make lots of changes to every link. Instead, we came up with an instance initializer to intercept location changes, convert the hash-based URLs to our new form asking the Ember router to do its thing.

// instance-initializers/backbone-route-interceptor.js
export function initialize(appInstance) {
  const router = appInstance.lookup('router:main');
  const hashLocation = appInstance.lookup('location:hash');
  hashLocation.onUpdateURL((url)=> {
    const BACKBONE_URL_PREFIX = '/#facesheet/';
    if (url.startsWith(BACKBONE_URL_PREFIX)) {
      const restOfUrl = url.substr(2);
      const newUrl = `/${restOfUrl}`;
      router.replaceWith(newUrl);
    }
  });
}
export default {
  name: 'backbone-route-interceptor',
  initialize
};

This code gets loaded once on Ember startup. It looks a bit complicated so let’s walk through it. First it loads the Ember router and saves it in a variable for use later when the URL changes. Then we use Ember’s HashLocation to monitor for changes to the URL hash and invoke our callback function. Inside this callback we check that the hash URL is one of our facesheet URLs. When matching, we remove the # and tell the Ember router to take us to the transformed URL.

With this in place we can test it out loading our page /facesheet/123456789/labs/12, seeing lab 12, then clicking a link to lab 13 being redirected to /facesheet/123456789/labs/13 and seeing lab 13. It all works!

What We’ve Learned So Far

We’ve been pleasantly surprised with how straightforward our rewrite has been so far. We’re following our ground rules and achieving our aims:

  1. Continue developing new features during the rewrite
    We have two development teams and while one team has been working on this the other has been developing new pages and changing existing ones in Backbone without having to worry about the Ember changes.
  2. Release the changes incrementally as we go — no big bang release
    After about 6 weeks our users are all on the Ember app and our Backbone router has been deleted. We released individual routes as soon as they were completed. However the performance cost of reinitializing the Ember app each time the user left and returned was too high so we didn’t turn it on until all the routes done. We did do a mini-big bang release for Phase I, but we think we’ll be able to be truly incremental and avoid that for the coming phases replacing the Backbone models and views.
  3. End up with an application architecture similar to what we would have come up with if we had started as a greenfield project
    We seem to be heading towards a clean Ember-CLI app even though we do have some temporary code to enable Ember and Backbone to interact. This has been essential in allowing us to release early and often. We are confident that will go away as we replace the remaining Backbone code with Ember.

Our Test Suite Saved our Bacon

We’re in the process of entirely rewriting our application which is big and scary. How can we know we didn’t break anything? We can’t click every link to make sure it works. Ah, but actually we can! Over the years we’ve invested in an extensive acceptance test suite that we’re now running against the new Ember application. It’s paying off big time!

It has caught a few edge cases we missed in our migration that we were able to quickly fix. Most importantly, its given us a high level of confidence that our new, evolving application works correctly. The only bug that has been reported in production was a button we intentionally removed because we thought it was unused. Once our users let us know they depended on it we were able to quickly add it back.

We’ve Gotten Speed for Free

We’ve been very pleasantly surprised by the huge performance improvement when changing routes. This is because of the model caching Ember provides for free. Even though we’re still using Backbone models not Ember Data when we switch from one lab to another in facesheet/labs/show the only model hook that re-fires is the show one. The ancestor facesheet and labs routes that load the patient and a summary of all labs are unchanged. We certainly could have built better caching into our old Backbone app, but that would have taken thought and work. Who wants to spend time worrying about that! A big advantage of Ember is that the framework has already thought of issues like this, so we don’t have to. It’s hard to argue with getting something like caching for free. We haven’t gotten much direct feedback from our users about the site being faster (even though we know it it is 😀). Instead, we have gotten complaints about other areas we haven’t touched being slower, which we’re interpreting as indirect feedback that the facesheet is faster.

Next Steps

We’ve completed Phase I of our rewrite and its been live in production for a few weeks with no reported issues. We’re embarking on Phase II to use Ember Data models to talk to our API followed by Phase III to convert our Backbone views to Ember components using Handlebars. We’re very happy with our current progress and can see the path toward ending up with a pure Ember app.

The Ember Architecture end state

The Ember Architecture end state

We’ll post follow-ups as we move through the other phases of this rewrite.