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.
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:
We went with option 2.
Method to the Madness
Now, the frontend just needs to blindly listen to the server, and render the HTML as instructed.
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:
- 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.
- 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:
- Market Timing? Not on the Betterment Apple Watch App
- Engineering at Betterment: Do You Have to Be a Financial Expert?
- Women Who Code: An Engineering Q&A with Venmo