Making Your Monolith Fly

Using Ember.js to Incrementally Extract Feature-Sets

null null null
Ticketfly Tech: More Than Code
10 min readSep 13, 2016

--

By SolFar on DeviantArt

So you’re working with a legacy system? A monolithic, server-rendered dinosaur with a vast and under-tested feature-set? If you’re at a company that’s over five years old, you probably know what I’m talking about. You probably also know that migrating this dinosaur into the new era of single page applications (SPAs) will make your application faster and more maintainable, but it still begs the question: how?

One approach would be to do it all at once: quietly overhaul your application and then release your shiny new 2.0 in a few months or years. This would be great if you’re

  1. alright with freezing new feature development, since adding a new feature to your 1.0 means you’ll have to duplicate the work in 2.0
  2. confident you could perfectly design 2.0 and then waterfall it into production. Stranger things have happened, right?

On the other hand, you could do your work incrementally:

  1. Select a feature-set
  2. Pull it out into a SPA
  3. Deploy it along with your monolith

After that, select another feature-set, perform that same happy magic, and delight as your monolith atomizes into dozens of neatly encapsulated SPAs, each one independently deployable and tested. Even better, you can use these same techniques to seamlessly deploy entirely new feature-sets along the way. It’s enough to make a T-Rex sprout feathers and take flight!

By Sterling Sheehy — Source: http://www.sterlingsheehy.com/2012/feathered-tyrannosaurus-rex-painting/

If you’re lucky, you’ll be able to just open these mini-SPAs in a separate tab, but what happens if you want your SPA to interact with the current page, such as a form or an older JavaScript app (Backbone, perhaps)? There’s a lot of state on these pages, and you can’t simply navigate away from it. Instead, you’ve got to devise something novel.

For us at Ticketfly, it meant breaking our feature sets into Ember addons, and then placing them in a full-screen modal so that they could seamlessly overlay the old interface without losing its state. To keep coupling loose, we triggered and listened to events. And the end product — after extensive experimentation — shined. We managed to hammer out something that works brilliantly in this scenario, and proud to say: here’s how we did it.

Two Paths to Paradise: Ember Addons and Islands

As the Ember team likes to put it,

Ember.js is a framework for creating ambitious web applications.”

It has a best-in-class router, a zippy build pipeline, and a truly flourishing ecosystem of addons. Despite a reputation for being an ‘underdog’ among the SPA frameworks, it’s used by Linkedin, Yahoo!, Travis, NASA, Twitch, and countless others. We decided to go with it just as it was entering 2.0, and it has proven itself a solid choice. The same systems that allow for it to support addons enable us to encapsulate feature sets and streamline deployment.

Path One: Ember Islands

The easiest way to start integrating Ember apps into your monolith is to use ember-islands. Because this approach is well documented by the ember-islands repo itself, we won’t go into the details here. Just know that it is by far simpler than what we do at Ticketfly, but it doesn’t meet one of our requirements: we need to build/deploy our Ember app separately from our monolith, but that isn’t an option with ember-islands.

Instead, we employ the approach outlined below. Compared to the ease of setting up an ember-island, our approach is painful. The upswing is that we don’t have to rebuild/redeploy our monolith every time we make a change to our Ember app, and eventually, once we’re ready to transition fully away from the monolith, we’ll be able to do so with just a few lines of code.

Path Two: Ember Addons

We have four requirements:

  1. UI/backend separation: allowing for each to be built/deployed on their own.
  2. UI continuity: the Ember UI must flow seamlessly away from the monolith and then back to it.
  3. Feature encapsulation: ensuring that teams can work on feature-sets independently and with as few side-effects as possible.
  4. Feature composability: allow feature-sets to be easily used in different contexts.
An Ember app defending its territory | art by Ryan Steiskal

With a little finagling, Ember can support requirements 1 and 2, but 3 and 4 are a different beast. The thing is, Ember is territorial. If you have an Ember app on your page, it will butt heads with (and ultimately knock itself unconscious against) any other Ember apps on the page. This presents something of a problem if our goal is to break our monolith into dozens of tiny SPAs, right? We need multiple Ember apps to crowd together onto a single page without butting heads.

Some day in the not too distant future, this problem will be solved by Ember Engines, which will allow us to compose multiple apps together. Until then, we’re doing the next best thing; we’re using Ember Addons. This approach grants us most of the encapsulation we’re aiming for. When we define components, templates, and even routes in our addon, they’ll be proxied directly into our app.

To get this working, input the following into the command line (CLI):

ember addon my-feature-setember addon my-other-feature-setember new my-consuming-app

This will create two addons, as well as an app to consume them. Once those are ready, publish all these repos to Github or wherever else you keep your code.

The consuming app should have no logic in itself, but rather just be a place to compose your feature-sets. Within your my-consuming-app directory, you can then perform the following (assuming use of GitHub):

ember install https://github.com/My_Organization/my-feature-setember install https://github.com/My_Organization/my-other-feature-set

(You might be familiar with a syntax more like ember install my-feature-set. This works if my-feature-set is published on NPM, but short of that, you can provide a url to the repo and that’ll work just as well.)

And now they’re all one big, happy family. Pretty much.

Primarily, we’ll be adding entirely new files to our addon, but things get a little trickier when we want to add something to a preexisting file, such as router.js. The answer is to still define your routes in the addon, then manually call them in the parent app. For instance:

// my-app/app/router.jsimport { myAddonRoutes } from 'my-addon';export default Ember.Router.extend({
myAddonRoutes(this);
});

Then modify the addon index.js:

// my-addon/addon/index.jsimport myAddonRoutes from './routes';export {
myAddonRoutes
};

Finally the addon routes.js:

// my-addon/addon/routes.jsexport default function(router) {
router.route('my-route');
};

If you’re using liquid-fire, you can do something similar with transitions.js. Start with your app:

// my-app/app/transitions.jsimport { myAddonTransitions } from 'my-addon';export default function() {
myAddonTransitions(this);
}

Then modify the addon index.js:

// my-addon/addon/index.jsimport myAddonTransitions from './transitions';export {
myAddonTransitions
};

Finally add a transition in the addon transitions.js:

// my-addon/addon/transitions.jsexport default function(lf) {
lf.hasClass('my-class'),
lf.use('toLeft', { duration: 100 })
}

You’ll definitely need to do this with your css, too. Luckily, sass makes this easy; just import it into app.scss:

// my-app/app/styles/app.scss@import "my-addon/my-addon-styles";

Don’t forget your my-addon-styles.scss:

// my-addon/app/styles/my-addon-styles.scss.my-addon-styles-namespace {
a {
color: purple;
}
}

Integrating with the Dinosaur

Integrating with the Dinosaur | Source: http://hyperboleandahalf.blogspot.com/2013/10/menace.html

Now that your addons are living harmoniously together in a single app, we start the next phase: integrating with the dinosaur. The specifics will vary greatly depending on your server-side app, but the gist is as follows. You need to get the compiled JavaScript and CSS from your app into the monolith somehow.

At the same time, we have to remember requirement 1: UI/backend separation. We want to be able to build/deploy the Ember app independently from the monolith, so even though we’ll be integrating with the dinosaur, we need to keep them loosely coupled.

At a high level, we do this by deploying the built Ember assets to S3, and then using a key-value store such as Redis or Zookeeper to keep track of the latest deployment. In our monolith, we can now simply query the key-value store for the url of our latest deployment and then plop that url into our monolith’s index.html. We can then deploy a new version of our Ember app without having to rebuild or redeploy the monolith. Beautiful!

To get this working, though, we’ve got to take care of a few details first. Let’s start by changing the way that Ember compiles its code. In your ember-cli-build.js file by mimicking the below configuration:

module.exports = function(defaults) {
var app = new EmberApp(defaults, {
storeConfigInMeta: false,
fingerprint: {
enabled: false
}
});
}

storeConfigInMeta

storeConfigInMeta: false

When storeConfigInMeta is true, it puts the contents of your config/environment.js into a meta tag in the Ember app’s index.html file. The problem is, we’re not going to use our Ember app’s index.html file. We’ve got to use the index.html file created by our monolith, since it’ll be the one hosting our app. By setting this value to false, we tell Ember to store the config in its compiled JavaScript instead.

Fingerprinting

fingerprint: {
enabled: false
}

As for fingerprinting, we turn it off because our monolith can include

<script src=”/assets/vendor.js”></script>

with ease, but will break every time we create a new build if we hard-code it to a fingerprint such as:

<script src=”/assets/vendor-1k2h5320912h4n89haienk5.js”></script>

The problem is, those fingerprints exist for a reason: to prevent your server from returning a cached version of the asset. You will have to use another technique to ensure that you’re serving fresh assets while creating unique urls — unique urls that your monolith somehow has knowledge of.

Our solution was to deploy our assets under a time-stamped namespace. We use Amazon S3 (though that detail could easily change), so our urls ended up like:

https://s3.amazonaws.com/my-bucket/20162306131017/assets/vendor.js
--or--
https://s3.amazonaws.com/my-bucket/20162307062354/assets/vendor.js

The Modal

Full screen modal transitioning between the Ticketfly box office — seat picker apps

Now that your Ember app lives inside your monolith, let’s get it to actually do something. The first step is to alter your config/environment.js to use hash routes:

module.exports = function(environment) {
var ENV = {
locationType: 'hash'
}
}

The hash routes will allow your Ember app to co-exist with the monolith. Typically, Ember’s router will take over the document’s location, but if you use hash routes, then it will respect the base route and simply append a ‘#’ followed by the Ember route. For instance:

// with ‘auto’
http://my-host.com/my-monolith-route/my-ember-route
// with ‘hash’
http://my-host.com/my-monolith-route/#/my-ember-route

Next, we nest the content of our routes inside a full-screen modal. To the user, it appears as though they’ve simply gone to another page, though in reality they remain where they were, just with a flashy new Ember app on the surface.

There are several excellent modal addons to choose from, all of which you can find on Ember Observer. We chose to go with liquid-tether, since it allows us to animate the modal entering and leaving the screen.

After that, you simply have to wrap the outlet of your entry-point template with the modal. So for instance, within the entry.hbs template:

// my-addon/addon/templates/entry.hbs{{#liquid-tether}}
<div class=”my-addon-styles-namespace”>
{{outlet}}
</div>
{{/liquid-tether}}

WARNING: You should avoid wrapping the application.js outlet, as this will result in the modal always displaying.

Monolithic Interactions

The last thing we have to worry about is interacting with the monolith, both intentionally and unintentionally. Obviously, we want to minimize unintended interactions, while keeping our intended interactions as decoupled as possible.

CSS

Let’s start by dealing with the unintended interactions, and specifically CSS interactions. You might have noticed that the {{outlet}} is wrapped in a div with a conspicuously named class, ‘my-addon-styles-namespace’. If you want to avoid polluting the monolith’s css with your Ember app’s CSS, you can simply place it all under this namespace. If you’re using SASS, it might look like:

// my-addon.scss.my-addon-styles-namespace {
.row {
. . . .
}
}

Transitions

To transition into the app, you simply have to change the url so that Ember notices a route change. For instance, if you want the user to click the ‘Raptor’ button to open your dino Ember app, it might look like this:

<button onclick="window.location.hash='entry/dino';">Raptor</button>

And when you want to exit the app, just do something like this:

this.transitionTo('index');

Easy peasy.

Communications

Finally, let’s get our monolith and Ember app talking. Since we want to keep these two decoupled, we use events to handle the conversation. From the Ember app you might trigger the event like so:

$(document).trigger('my-ember-app-did-something', ['some', 'params']);

And then in the monolith:

$(document).on('my-ember-app-did-something', function(...args) {
. . . .
});

Conclusion

Despite the challenging setup, once you get to this point, development will start to move faster. Since we’ve gone to great lengths to keep the monolith and our Ember app separate, we’ll be able to test and develop our app with greater independence than a monolith typically affords. And in time, we’ll be able to scrap the monolith all together, replacing it wholly with Ember.

--

--