Webinar: Why logical layers matter, and how to use them -Watch now

Learning Mode’s Ruby codebase through its Gemfile

Image of author
Max Edmands, Engineering Manager

September 9, 2019

4 minute read

What is the most efficient way to wrap your head around an unfamiliar codebase?

There are lots of fantastic options. Some examples: you can read the documentation, look at the output of the test suite, and dig through the directory structure. You can look at recent GitHub pull requests and see what’s changed recently. If it’s a Rails app, you can peek at the models to understand the domain concepts, or the controllers to see what the API looks like. You can pair with someone who’s familiar with the code and see what they do, or they can give you a high-level overview of the moving parts with a whiteboard.

Many of the above strategies were a great help to me when I took my first look at the code here at Mode. But one lens that I found particularly interesting was digging through the Gemfile. As it turns out, our system's dependencies told me a lot about our architecture, assumptions, and style!

Continuing in the spirit of our previous blog post, Gems We Love, here are some of the Ruby gems in our stack that I found particularly interesting and insightful:

Interactor - This gem allows us to encapsulate our app's business logic in a home completely outside of models, views, and controllers. We actually have another directory to hold them, app/interactors, which is a great place to start if you're trying to answer a question like "How do we provision a new account?" or "How do we log users in?" or "How do we run a query?" Since our business logic is decoupled from I/O and persistence, it's easy for us to keep things readable and DRY, following the Single Responsibility Principle, as we evolve and grow.

Halogen - Halogen was written by our CTO Heather Rivers a few years ago and Bindu also called it out as a favorite in Gems We Love. This gem is how we organize our API presentation layer. We expose resources in our HTTP API as HAL-compatible JSON resources - this means that every JSON document returned by our API contains links (also known as "hypermedia references") to related resources that are accessible from different API endpoints. For example, if you load a user object in our API, it comes with a link to load the reports that the user created, as well as the memberships that the user has to various organizations. See our developer documentation for some examples of how this looks in practice!

Paranoia - This gem provides us with the ability to soft-delete our ActiveRecord models. So long as a model is defined with the acts_as_paranoid method and has a deleted_at field, using the destroy method on it doesn't delete the record from the database, but instead updates the deleted_at field on the model with current timestamp. It also sets a default scope so that rows with deleted_at don't get returned by queries unless you really, really want them to. (Note: much has been said on the internet about the implicit dangers of using this pattern -- but it remains one of the most straightforward and lightweight ways to soft-delete records, and so it's part of our codebase. Like everything else in engineering, it's a judiciously-applied tradeoff.)

AASM (Acts As State Machine) - With this gem we can turn our ActiveRecord models into finite-state machines with well-defined transitions. For example, a "Query Run" in Mode can be pending, enqueued, cancelled, failed, or succeeded. AASM allows us to easily define an enqueue method on the QueryRun class, which raises an error unless the query run is in the pending state, and atomically updates the state to enqueued. This ensures that we can only enqueue a Query Run once:

**Ancestry** — This gem turns an ActiveRecord model into a collection of nodes in a hierarchical tree structure, complete with accessors like parent, children, or siblings. All we need to do to use it is add an ancestry column to the database table and a has_ancestry call in the class definition. It works by storing the entire ancestry path, from the root, for every row in the database - so it is capable of fetching an entire subtree with a single database query. Fancy!

**ranked-model** - Gives us flexible sorting options for some of our models. For example, the individual cells in our hosted Python notebooks can be re-ordered by a user. We use a database column called row_order for this - but the specific values in this column aren't the important thing - rather it's the values relative to each other that are important. If there are three cells in the same notebook with row_orders of 1, 2, and 3, that's materially the same as if row_order were 5, 10, and 15. ranked-model lets us do ignore the specific values and focus only on the thing that matters - the relative ranking.

**Kaminari** - This gem provides a simple interface by which we can paginate our ActiveRecord calls. So long as kaminari lives in the gemfile, all ActiveRecord models have accessors like page() that allow us to return a specific page of results. It's worth noting that this library uses offset-based pagination rather than cursor-based pagination, which is more complex but also more reliable if the result set is large or changes frequently.

**Figaro** - We use this gem for twelve-factor-app-compatible configuration management. According to the twelve-factor philosophy, if there is ever something that causes an app to vary between deploys (e.g., things like log volume, database connection strings, etc), that configuration should live in environment variables. Figaro is a convenient way for us to define the limited environment variables our app cares about, as well as setting defaults so things work as expected in development.

This is but a sampling of the many excellent open source Ruby libraries we use to make Mode work.

As always, if you’re interested in working with us, take a look at our careers page.

Get our weekly data newsletter

Work-related distractions for data enthusiasts.