Typing a 2-Million-Call Ruby Monolith

Shopify's core application is large: roughly 37,000 Ruby files containing over 2 million method calls. With about 400 commits merged daily and 40 deployments per day, developers need a tight feedback loop to keep the system stable. Since 2018, the Ruby Infrastructure team has looked for ways to deliver that feedback faster and with more confidence. Their chosen route is static typing via Sorbet.

The State of Ruby Static Typing at Shopify

At a November 2020 Shipit! event, Shopify engineers Alexandre Terrasa and Ufuk Kayserilioglu discussed how the company adopted Sorbet and what remains unsolved. The presentation generated more questions than they could answer live; the following covers the main points of that discussion.

The Hard Parts: Concerns, Hooks, and Include Sites

Modules are the most difficult Ruby code to migrate to Sorbet. Modules built on ActiveSupport::Concern that assume inclusion in a particular kind of class create special problems. If a module calls before_save, assuming it will be included in an ActiveRecord model, Sorbet has no way to know where before_save is defined. The team is looking to make those inclusion-requirement dependencies between modules and their include sites explicit in Sorbet. The details of that work are in a Sorbet pull request.

Extremely dynamic metaprogramming-heavy codebases are also weak targets for Sorbet. Even running at typed: false yields some benefit, but unless the team is behind the effort, using Sorbet can turn counterproductive. If a team cannot be convinced of its utility, the engineers advise not forcing the issue.

What Sorbet Can and Cannot Check

For an array argument, the level of checking depends on what you specify as its element type. Typing it as T::Array[Integer] gives full checking of method calls. More complex structures can be typed with increasing precision — from T::Array[T.untyped] to nested hashes all the way down to T::Array[T::Hash[String, T::Array[Integer]]]. Whatever you specify, Sorbet checks what it knows about. Once an array type grows too unwieldy, it is usually a signal to model the structure as its own class.

Comparing Sorbet for Ruby with TypeScript for JavaScript, the engineers note that Ruby is considerably more dynamic than JavaScript and that Sorbet is younger than TypeScript. Coverage of Ruby features and expressiveness therefore lag behind what TypeScript brings to JS. On the other hand, Sorbet annotations remain pure Ruby operators: no new language to learn, no compilation step from typed Ruby to plain Ruby, and the existing editor tooling keeps working. Sorbet also ships a runtime type checker that validates signatures during application execution — extra safety TypeScript lacks.

Sorbet vs. RBS and the Ruby 3.0 Question

On RBS versus Sorbet, the team points to Stripe's comparison post. RBS defines a signature language, but you still need a type checker to run those signatures against code; Sorbet is the fastest such checker today. A limitation of RBS is having no inline annotation syntax — you cannot cast a variable in place, so type checkers must resort to extra extensions. Sorbet does not currently consume RBS, but it might in the future. If not, the migration from one typed format to another remains easier than from an untyped codebase.

No official roadmap exists for converting RBI to RBS files once Ruby 3.0 ships; that work is on the side while Shopify moves its monolith to 100% typed: true files. Some of the differences between the two specs are cataloged in Shopify's rbs_parser project.

Tooling: Tapioca, Enums, Editors, and GraphQL

Tapioca, Shopify's RBI generator, can emit definitions for T::Enum classes that come, and it has DSL generators that define methods for ActiveRecord enums. For help moving a Rails project up the type-strength ladder, they point to the Sorbet site's quickstart steps plus sorbet-rails for Rails RBIs and tapioca for gem RBIs.

The recommended starting point is to set every file to typed: false first. Using Tapioca at that level has a low cost and already prevents a fair number of errors. From there, move files to typed: true only where no new type errors appear; spoom includes a utility for changing the sigil in files. Signature prioritization should follow usage: highly reused files, files generating the most production errors, files with heavy churn, or files edited by many teams. All generated RBI files should be committed to the repository so every developer working on the codebase gets type checking.

Shopify has not standardized on one editor, though the developer tooling group focuses on VSCode support; one of the engineers uses VSCode while the other prefers VIM. GraphQL poses an ongoing challenge: marrying GraphQL's types to Sorbet's via RBI generation stalled over the dynamic nature of resolvers. Teams that rewrote their GraphQL endpoints to work more naturally with Sorbet have succeeded; one engineer documented that approach in a blog post on GraphQL, Sorbet, and unit tests.

Further Reading