engineering

Betterment engineers (l-r): Arielle Sullivan, J.P. Patrizio, Harris Effron, and Paddy Estridge

We recently changed the way we organize our major business objects.

All the new features we’re working on for customers with multiple accounts—be they Individual Retirement Accounts (IRAs), taxable investment accounts, trusts, joint accounts, or even synced outside accounts—required this change.

We were also required to rename several core concepts, and make some big changes to the way we display data to our customers.

Currently, our Web application is a JavaScript single-page app that uses a frontend MVC framework, backed by a JSON api. We use Marionette.js, a framework built on top of Backbone.js, to help us organize our JavaScript and manage page state.

It was built out over the past few years, with many different paradigms and patterns. After some time, we found ourselves with an application that had a lot of complexity and splintered code practices throughout.

The complexity partly arose from the fact that we needed to duplicate business logic from the backend and the frontend.

By only using the server as a JSON API, the frontend needed to know exactly what to do with that JSON. It needed to be able to organize the different server endpoints (and its data) into models, as well as know how to take those models and render them into views. For example, a core concept such as “an account has some money in it” needed to be separately represented in the frontend codebase, as well as the server.

This led to maintenance issues, and it made our application harder to test. The additional layer of frontend complexity made it even harder for new hires to be productive from day one.

When we first saw this project on the horizon, we realized it would end up requiring a substantial refactor of our web app. We had a few options:

  1. Rewrite the JavaScript in a way that makes it simpler and easier to use.
  2. Don’t rewrite JavaScript.

We went with option 2.

Instead of using a client side MVC framework to help enable us to write a single page app, we opted to use our Rails server to render views, and we used server generated JavaScript responses to make the app feel just as snappy for our customers. We achieved the same UX wins as a single page app with a fraction of the code.

Method to the Madness

The crux of our new pattern is this: We use Rails’ unobtrusive JavaScript (ujs) library to declare that forms and links should be submitted using AJAX. Our server then gets an AJAX rest request as usual, but instead of rendering the data as JSON, it responds to the request with a snippet of JavaScript. That JavaScript gets evaluated by the browser. The “trick” here is that JavaScript is a simple call to jQuery’s html method, and we use Rails’ built-in partial view rendering to respond with all the HTML we need.

Now, the frontend just needs to blindly listen to the server, and render the HTML as instructed.

An Example

As a simple example, let’s say we want to edit a user’s home address. Using the JavaScript single page app framework, we would need a few things.

First, we want an address model, which we map to our “/addresses” endpoint.

Next, we need a View, that represents our form for editing the address.

We need a frontend template for that view.

Then, we need a route in our frontend for navigating to this page.

And for our server, we need to add a route, a controller, a model, and a jbuilder to render that model as JSON.

A Better Way

With our new paradigm, we can skip most of this. All we need is the server. We still have our route, controller, and model, but instead of a jbuilder for returning JSON, we can port our template to embedded Ruby, and let the server do all the work. Using UJS patterns, our view can live completely on the server.

There are a few major wins here:

  1. Unifying our business logic.
    The server is responsible for knowing about (1) our data, (2) how to wrap that data into rich domain models that own our business logic, (3) how to render those models into views, and (4) how to render those views on the page. The client needs to know almost nothing.
  2. Less JavaScript.
    We aren’t getting rid of all the JavaScript in our application. Certain snappy user experience elements don’t work as well without JavaScript. Interactive elements, some delightful animations, and other frontend behaviors still need it. For these things, we are using HTML data elements to specify behaviors.

    For example, we can tag an element with a data-behavior-dropdown, and then we have some simple, well organized global JavaScript that knows how to wrap that element in some code that makes it more interactive. We are hoping that by using these patterns, we can limit our use of JavaScript to only know about how to enhance HTML, not how to automatically calculate net income when trying to distribute excess tax year contributions from an IRA (something that our frontend JavaScript used to know how to do).

  3. We can do this migration in small pieces.
    Even with this plan, migrating a highly complex web application isn’t easy. We decided to tackle it using a tab-by-tab approach. We’ve written a few useful helpers that allow us to easily plug in our new server-driven style into our existing Marionette application. By doing this piecemeal, we are hoping to bake in useful patterns early on, which we can iterate and use to make migrating the next part even simpler.

If we do this right, we will be able to swap everything to a normal Rails app with minimal effort.

Once we migrate to Rails 5, we should even be able to easily take advantage of Turbolinks 3, which is a conventionalized way to do regional AJAX updates.

This new pattern will make building out newer and even more sophisticated features easier, so we can focus on encapsulating the business logic once.

Onboarding new hires familiar with the Rails framework will be faster, and those who aren’t familiar can find great external (and internal) resources to learn it. We think that our Web app will be just as pleasant to use, and we can more quickly enhance and build new features going forward.

More from Betterment: