Enforcing Public/Private Access in Rails

Ethan Pailes
Open House
Published in
5 min readSep 10, 2020

At Opendoor, we have a large Rails monolith, which we used early on to build our core business, and a fleet of supporting microservices, which is where we focus most of our new development effort. We began migrating to a microservice architecture in part because, as the number of our engineers grew, it became harder and harder to prevent different modules from bleeding into one another without regard for ownership boundaries. Of course, standing up a few microservices did not make all of our existing code disappear, and much of the business value of our systems still lives in the monolith, so we still need to be able to work effectively with it.

Isolating Our Construction Service

After we buy a home from a customer, we usually perform a few repairs to get the home ready to be listed for customers to tour and buy. This process has historically been managed by our Rails monolith, but we recently built a new set of tools for managing construction projects on homes. Though operators primarily interact with the new system through a new web based UI and a mobile app that we shipped as part of the migration, there are some places where operators continue to interact with the construction process via the UI served by our monolith.

When we were first considering how to teach our Rails monolith to talk to the new construction service, we wanted to make sure that we had a way to ensure we had updated all references to the old data models to work with the new service’s API. To do this, we decided to create a module within our monolith that could present the new data in a form which looked like the old Rails models.

One of our pain points with our Rails monolith is that Ruby’s dynamic types make it difficult to reason about code in isolation. In particular, it is very difficult to make code private to a particular module or domain in Ruby. Nevertheless, we wanted to see what we could do to enforce privacy constraints a little more powerfully than the private keyword allows.

Rails Engines

We first thought that this situation could be a good fit for using a Rails engine. We had heard good things about using Rails engines from some former Flexport engineers, who had had good experiences with using Rails engines to modularize their monolith, and, importantly for our use case, they knew about some neat rubocops that Flexport had built to ensure that Rails engines stay isolated. No one at Opendoor had yet used a Rails engine in our monolith, so we would be breaking a little snow, but we were not too worried.

One of our engineers set to work adding the skeleton of a new Rails engine for the new construction module and was immediately struck by just how heavy a Rails engine was for our use case. A Rails engine can best be thought of as a Rails app packaged as a library. Just like a Rails app, a Rails engine has models, controllers, and views. This is very powerful because it allows engines to deliver an end-to-end solution, providing everything from the UI to the database layer, and do so in a way that does not pollute the app importing the engine. For our use case, however, it was a little overkill. We didn’t need a UI. We didn’t need models. We didn’t need controllers. We did want to provide utilities that would allow the rest of our monolithic app to talk to our new service over GRPC, but this use case was not really aided by any of the abstractions provided by a Rails engine. The only thing that we really wanted a Rails engine for was Flexport’s slick rubocops.

Pivot: Rolling Our Own Lint

We couldn’t help but feel like it was a little silly to use an abstraction that we had no use for simply because it already had good tooling. The unfinished diff to add an empty Rails engine was huge even after ripping out many of the files we would not need. We wondered how hard it would really be to write a custom rubocop to enforce the isolation of a particular subtree of our main Rails app. It turned out that the answer was: “not that hard.” We were able to quickly write a rubocop to ensure that there were no references to Ruby modules defined in a particular subtree from outside of that subtree. The only way the outside world is allowed to talk to one of these isolationist source trees is to use methods defined in a module named API or a submodule of it.

It ended up being less effort to write a lint tailor fit to our use case than it would have been to integrate a Rails engine, and the final product was significantly simpler. We now have more confidence that the construction module we built is well encapsulated, and, perhaps more importantly, we have confidence that it will remain that way over time. Without our custom lint, we would have had to make sure to continually communicate the intended isolation grantees of the new module to every other team that interfaces with construction code.

Project Specific Lints

Leaning on static analysis and linters is something that we do a lot at Opendoor, but we were still a little surprised by how easy it was to build a custom lint to fit our particular need. It is easy to imagine how other custom lints could be useful. In our case, we wanted to impose a little discipline on Ruby’s free-spirited nature, but there are plenty of other possibilities such as:

  1. Preventing new usages of a deprecated internal API unless they are added to an explicit allowlist.
  2. Making changes to a particular enum definition without updating a corresponding table.
  3. Using a language construct that the team has decided does more harm than good.

There are a lot of high quality generic lints floating around out there that you should absolutely use to improve code quality, but each project has its own domain specific rules that must be followed. If it is possible to encode these rules in a machine-enforceable way, at least consider doing so. It may be easier than you think, and it will make your application easier to reason about. You don’t always need to rely on your language to provide guard rails; sometimes you can build them yourself.

--

--