Terraforming legacy Rails applications, by Vladimir Dementyev

Abstract

Rails has been around for (can you imagine!) about 15 years. Most Rails applications are no longer MVPs, but they grew from MVPs and usually contain a lot of legacy code that "just works."

And this legacy makes shipping new features harder and riskier: the new functionality have to co-exist with the code written years ago, and who knows what will blow up next?

I've been working on legacy codebases for the last few years, and I found turning legacy code into a legendary code to be a very exciting and challenging process.

I want to share the ideas and techniques I apply to make legacy codebases habitable and to prepare a breeding ground for the new features.

Details

The mechanics of this talk is based on a popular board game–Terraforming Mars.

The goal of the game is to make Mars habitable.
The goal of the talk is to show how to make legacy codebases a better place to build new features.

The game flow inspires the narration.

Turn #1. Mars Landing

At the beginning of the game, we land on Mars to do some research work and plan future actions.

The best way to explore a legacy application is to make it up and running locally.
And this step is not that simple as it seems to be: the documentation/Readme could be outdated, external/system dependencies are tricky to install, configuration parameters are kept secret (i.e., stored only inside someone's head).

During this step we create a Docker-based development environment: first, to simplify the onboarding for future devs, secondly, to reveal the app configuration problems–making the codebase 12factor-able reveals configuration coupling (=hardcoding) problems.

Turn #2. Covering Mars with oceans tests.

We have configured our development environment. Now let's try to run our tests (if any).

What do we want from our tests? We want them to be reliable.

We need minimal coverage: we should figure out the golden path and make sure that it's covered at least with integration/system tests.

We need our tests not to be slow: we run tests more often in the early days when working on a new project, we don't want them to have a significant effect on our feedback loop.
Here I'd like to give a sneak preview (the topic worth its own talk) on how to analyze the test suite using specific profilers, apply some patches to heal the bottlenecks (e.g., getting rid of database_cleaner in favor of transactional tests).

We want our tests not to share global state (and thus be flaky). There are some typical cases of global state: database, uploaded files, test background jobs, cache, time traveling. I want to show how to detect and fix/prevent these problems with a little amount of code.

Turn #3. Planting of greenery.

Server is running, tests pass. We're ready to investigate the code itself.

We start with the dependencies:

  • checking for known CVEs in dependencies
  • checking for outdated patch/minor versions (no major upgrades)
  • comparing local patches to upstream versions removing github dependencies from Gemfile when possible.

The goal of this turn is to make our deps as up-to-date as possible without a lot of effort (so, minor breaking changes is OK). That would help us to do major upgrades in the future.

Let's raise the oxygen level and do some code audit!

Here I want to show how to detect security problems, performance issues (e.g., N+1), dangerous side-effects (like DB transactions atomicity violations or inconsistent DB state).

We also take a look at our configuration and make sure it's consistent (e.g., DB pool size vs. Sidekiq threads count).

Turn #4. Atmosphere.

Right before we started writing code, the last step we do is making sure that atmosphere we've built is protected from harm.

We need to enforce the new order–we need cops "linters"!

Here I want to talk about how to add linters to the existing projects without rewriting everything (progressive linting), what tools we use to make development/review process automized.

Except form enforcing rules, we want to monitor the application health in general: we add instrumentation, collect metrics, configure dashboard and alerts. We don't need surprises.

End of Game. Life on Mars.

We made our app a good (enough) place to live code.

Here is the quick overview of what we did (The Checklist) – make sure you have all items checked in your app!

This talk is about, IMO, every developer should know to keep their application healthy; thus it fits anyone from newcomers to advanced Rubyists (though, I think, the latter know most of these techniques, it's would be good to refresh this knowledge).

Pitch

I'm dealing with legacy code every day for the last three years. Every time I joined a new project (there were three of them during the period) I had to repeat almost the same actions (described in the talk). Why so?

Probably, due to Ruby and especially Rails ability to build and ship things faster than any technology. We usually sacrifice quality for development speed. That's strategy is not that bad, I'd instead say "good." But it's easy to miss the point in time when you lose control of your code–your code becomes a scary legacy monster 👻.

The goal of this talk is to demonstrate how to keep legacy code under control.

I don't want to talk about rewriting everything using yet-another-hispter-artchitecture-design and be happy. It's great to know that there is a better world somewhere, but it doesn't help to solve your existing app problems.

This talk would be particularly useful for consultancy agencies and independent contractors/freelancers, who work mostly with legacy code.
And even if you just started working on a new Rails application, it's good to know how to keep it habitable.

My personal goal is to avoid repeating all the preparation the next time I start working on a new project because the project would have all the techniques I put in the talk applied.

Edit proposal

Submissions

RailsConf 2019 - Accepted [Edit]

Add submission