Sorbet Journey

This is an ongoing series about adding Sorbet to a mature Rails code base. Follow me on Twitter to find out when updates are posted.

TL;DR

Working on a big code base without types is hard. We’ve been interested in Sorbet for a while but were waiting for the “right time.” As of late 2020 we’re going to give it a real try.

What’s the Problem

I oversee a ten-year-old “majestic monolith” Rails application. The app is Tickit, an event ticket sales system (a competitor to Eventbrite and Brown Paper Tickets). It deals with money in various currencies, events in various timezones, sales over various channels, and generates tickets according to various rules.

The app is around 100k lines of code and it is decently factored given its age. There are lots of high quality tests. Even so, it’s been getting harder and harder to change the app with confidence. A typical change to any business logic involves re-building a mental map of the data flowing from storefront to the backend, often wrapped up in hashes like this:

{
  12356 => { quantity: 2, freeform_base_price: '125.50', delivery: 'digital' },
  44123 => { quantity: 5, delivery: 'digital' }
}

And then they get decorated with payment requirements…

cart.grand_total # => Money.new(123_45, 'USD')
cart.display_fees # => Money.zero('USD')

Which are sometimes modified as cents…

cart.gratuity_cents = 10_00

And payment methods…

{
  payment_source: { name: '', payment_type: 'apple_pay' }
}

And seat selection…

{
  seat_selection_mode: 'manual',
  seat_qids: %w[e1-a e1-b]
}

Every request involves building up data structures from request payloads, passing them down through the business logic and back to the user, transforming them as we go. At every step we need to be sure that all the transforms that have happened are typesafe and appropriate. Pretty standard stuff, but time consuming nonetheless.

Our automated tests were extensive and gave us confidence, but a full CI run can take ten minutes and that feedback loop is too long. We want to be warned immediately if we try to put a Money object where an Integer is meant to be.

We Got a Taste of Typescript

We started converting our vanilla JS and CoffeeScript over to TypeScript about a year ago and it has been a fantastic win. Other than some tooling issues, day-to-day development using TypeScript has added a huge layer of confidence and increased code quality.

An unexpected bonus was IDE integration: after years of being a Sublime Text minimalist, moving to VS Code and getting auto-complete when writing TypeScript has been amazing.

Anytime we’ve had to write JS instead of TypeScript it has felt like a big step backwards. Writing Ruby without types was starting to feel the same, too.

Just Add Types

We experimented with Sorbet when it first came out, but found too many rough edges when adding it to our code base. There was a lot we were excited about, but we didn’t have the resources to pursue it then. We filed it away for future exploration.

This November, Shopify published a blog post about adopting Sorbet and hosted a webinar focusing on their Tapioca library. If Shopify can run Rails master and Sorbet, we figured, maybe it was ready for us to try with our more, uh, modest resources.

Rather than starting with our main app, we decided to start with a single private gem of ours to get a feel for Sorbet at its various strictness levels. If that went well, we could look at adding Sorbet to the main Rails app. So that’s where we began.