https://stripe.com/blog/sorbet-stripes-type-checker-for-ruby * Products * Use cases * Developers * Company * Pricing Sign in Dashboard Sign in Products * Atlas Startup incorporation * Billing Subscription management * Capital Business financing * Checkout Pre-built payments page * Climate Carbon removal * Connect Payments for platforms * Corporate Card Spend management * Elements Customizable payments UIs * Identity Online identity verification * Invoicing Online invoices * Issuing Card creation * Payments Online payments * Payment Links No-code payments * Radar Fraud & risk management * Revenue Recognition Accounting automation * Sigma Custom reports * Tax Sales tax & VAT automation * Terminal In-person payments * Treasury Banking-as-a-service Use cases * Ecommerce * SaaS * Marketplaces * Platforms * Creator Economy * Crypto * Pricing * About Stripe * Enterprise * Jobs * Partners * Newsroom * Documentation * Sessions * Blog * Support Sign in Payments * Payments Online payments * Checkout Pre-built payments page * Elements Customizable payments UIs * Payment Links No-code payments * Connect Payments for platforms * Invoicing Online invoices * Billing Subscription management * Terminal In-person payments Financial services * Corporate Card Spend management * Capital Business financing * Issuing Card creation * Treasury Banking-as-a-service Business operations * Radar Fraud & risk management * Sigma Custom reports * Atlas Startup incorporation * Climate Carbon removal * Tax Sales tax & VAT automation * Identity Online identity verification * Revenue Recognition Accounting automation * Ecommerce Unify online and in-person payments * SaaS Manage recurring billing and subscriptions * Marketplaces Pay out globally and facilitate multiparty payments * Platforms Let customers accept payments within your platform * Creator Economy Facilitate on-platform payments and pay creators globally * Crypto Enable seamless payments for your crypto business * Documentation Start integrating Stripe's products and tools Get started * Prebuilt checkout * Libraries and SDKs * Plugins * Code samples Guides * Accept online payments * Manage subscriptions * Send payments * Set up in-person payments * Full API reference * Support * API status * API changelog * About Stripe * Customers * Enterprise * Jobs * Newsroom * Partners * Sessions * From the blog * Stripe's payments APIs: the first ten years * European payment methods available worldwide * We built Checkout so you don't have to Solutions Payments Accept payments online, in person, or through your platform. Business operations Automate fraud prevention, tax, revenue management, and more. Financial services Launch your business, get financing, and manage expenses. Products for platforms Embed financial services in your platform, marketplace, or business. * Payments Online payments * Checkout Pre-built payments page * Elements Customizable payments UIs * Payment Links No-code payments * Terminal In-person payments * Connect Payments for platforms * Billing Subscription management * Invoicing Online invoices * Radar Fraud & risk management * Tax Sales tax & VAT automation * Revenue Recognition Accounting automation * Sigma Custom reports * Identity Online identity verification * Billing Subscription management * Climate Carbon removal * Atlas Startup incorporation * Capital Business financing * Corporate Card Spend management * Connect Payments for platforms * Capital for platforms Business financing * Issuing Card creation * Treasury Banking-as-a-service * Terminal In-person payments * Ecommerce Unify online and in-person payments * SaaS Manage recurring billing and subscriptions * Marketplaces Pay out globally and facilitate multiparty payments * Platforms Let customers accept payments within your platform * Creator Economy Facilitate on-platform payments and pay creators globally * Crypto Enable seamless payments for your crypto business * Global Run your business worldwide * Documentation Start integrating Stripe's products and tools Get started * Prebuilt checkout * Libraries and SDKs * Plugins * Code samples Guides * Accept online payments * Manage subscriptions * Send payments * Set up in-person payments * Full API reference * Support * API status * API changelog * About Stripe * Customers * Jobs * Newsroom * Blog * Stripe Press * Sessions Support * Professional services * Support Partners * Become a partner * Partner directory 1. Blog 2. Engineering Stripe on Twitter Sorbet: Stripe's type checker for Ruby March 28, 2022 [jake_] Jake Zimmerman Developer Productivity By 2017, Stripe had grown to the point where hundreds of engineers had written millions of lines of code. Most of that code was--and still is--written in Ruby, which is famous for helping engineers iterate quickly (if somewhat notorious for encouraging inscrutable code). Unfortunately we were starting to see Ruby come apart at the seams: new engineers found it hard to learn the codebase, and existing engineers were scared to make sweeping changes. Everyone faced a constant tradeoff: run the fast, local tests which might not catch many breakages, or run all the tests, even the slow ones. Ruby was becoming a source of friction more than a source of productivity. We set out to change that, with two goals in mind: make it easier to understand the code, while doubling down on what makes Ruby productive and delightful. This was the backdrop against which we decided to create and open source Sorbet, a fast, powerful type checker designed for Ruby. Sorbet statically analyzes a codebase, builds up an understanding of how each piece of code relates to every other piece, and then exposes that knowledge to the programmer via type errors, autocompletion results, documentation on hover, or jumps between definitions and usages. Today Sorbet runs over Stripe's entire Ruby codebase, currently amounting to over 15 million lines of code spread across 150,000 files. We can't take credit for pioneering the idea of adding static types to a dynamically typed language--Microsoft and Facebook popularized the approach with TypeScript and Hack, respectively. However, we thought it was worth sharing how Sorbet has not just met but exceeded our goals in the almost four years since we first enabled it on our Ruby codebase. Sorbet reinforces the delightful bits of Ruby while making engineers more productive. Not only has it made code easier to understand, it's even helped shape and reinforce Stripe's engineering culture as we've grown. But before we dive into what makes Sorbet... Sorbet, let's take a short step back in time to its origins at Stripe. A brief history of Sorbet inside Stripe Type annotations arrived in Stripe's Ruby codebase as early as November 2016, almost a full year before work began on Sorbet. These annotations were born out of a desire to encourage engineers to write modular units with clear public interfaces. Here's an example test case from the pull request that introduced type annotations: Blog > Sorbet > Declare method syntax image Neither Sorbet nor any other static type checker existed to consume these type annotations yet; they existed only at runtime. The declare_method call above acted like a decorator on the def call method: it would check that the msg argument given to call was a String and that call returned a String on every invocation. Throughout the next year, these runtime-only annotations spread throughout Stripe's codebase. Months prior we had added Flow, a static type checker for JavaScript, to our frontend codebase. Ruby developers quickly grew envious and kept asking us what it would take to get the same features for Ruby. We staffed an effort to figure out what it would take to either adopt one of the two in-progress Ruby type checkers--RDL and TypedRuby--or to build our own. RDL proved to be powerful, but too slow^1. TypedRuby was faster, but had bugs that would have required a near-rewrite to solve^2. So in November 2017, we began writing Sorbet from scratch. Six months later, in May 2018, Sorbet type checking became required in Stripe's automated test suite. After another year of internal adoption, we released Sorbet to the world in June 2019. In all that time a lot has changed--Sorbet has far more features today than we ever imagined back then. But there's been one constant driving force behind the project: building tools that make engineers working in Ruby more productive. Supercharged productivity in Ruby When we ask how Sorbet makes people more productive they tell us all sorts of things, but the most common theme is raw speed. Sorbet gives near-instantaneous feedback while editing: for 80% of edits, it can finish reporting type errors in milliseconds, even in our multi-million line codebase. The longest error reporting wait times measure in seconds. Types aren't a replacement for tests, but few test suites are fast enough to run on every edit like Sorbet. But there's more to it than just speed: Sorbet takes the toil out of understanding how code fits together. On the day we rolled out the Sorbet-powered VS Code extension for Ruby, Justin Duke described the feeling better than anyone: Having just spent the past few minutes clicking around VSCode like a kid on Christmas morning, I don't think it's an exaggeration to say that this might be the single largest improvement in my pay-server [Stripe's Ruby codebase] productivity since joining Stripe. Justin Duke, Satisfied Sorbet user In a large codebase, Ruby can be uniquely hard to understand, even among other dynamically typed languages. What's worse is that it's hard to just, say, lint against the features that make Ruby hard to understand, because many of them are Ruby's most loved features. Here are some of the features that can make a Ruby codebase hard to unravel: Ruby lacks import statements (like those in Python or JavaScript), which bind global names to file-scoped names. Instead, Ruby provides require statements, which merely run other Ruby code. This mechanism works kind of like #include statements in C and C++: a single require statement might hide implicit calls to hundreds of other require statements. But this feature enables Rails' famous "convention over configuration" approach to project layouts, which many people love about it, you don't have to import files in Rails, you can just reference the code you want to reference. Ruby encourages factoring code into modules, which can then be mixed into classes or even other modules. When used well, modules can help organize code into composable, testable units. But on the other hand, overuse of modules obscures where a method is defined behind a deep ancestor hierarchy. New Stripe engineers working in our codebase frequently struggled to find a method's definition when it came into scope from behind multiple layers of modules. Ruby embraces metaprogramming, which is when methods and objects are dynamically created by code itself, instead of directly by the programmer. Concretely, this means that while some methods are written literally like def invoices; ...; end, others are defined dynamically by calling a library function like has_many(:invoices). Metaprogramming as a way to share code is one of the biggest reasons why projects like Rails have been so successful. Unfortunately, metaprogramming is very opaque. It prevents simple regular expression searches from surfacing method definitions. Once a definition is found, the programmer still has to trace through code to know things like what arguments the method takes. We built Sorbet to make it easy to navigate and understand a codebase without having to give up these features people love about Ruby. The key, more than just reporting type errors quickly, is to offer a powerful editor extension, which provides ever-present answers to common questions. The answer to "where is this class defined?" is a click away, not hidden behind multiple require statements. "How am I supposed to use this method?" fades as a flick of the cursor reveals the method's types and documentation, replacing a lengthy crawl through a class's transitive mixins. Instant responses from Sorbet mean less time toiling and more time discovering. Building Sorbet in a way where it delivers type errors and IDE responses so fast comes from a set of design choices we made early on in its development. First, Sorbet is written in C++, not Ruby. To quote Nelson Elhage, one of the founding members of the team, "Writing in C++ doesn't automatically make your program fast, and a program does not need to be written in C++ to be fast. However, using C++ well gives an experienced team a fairly unique set of tools to write high-performance software." C++ gives us great baseline performance and a lot of headroom for further improvement when we decide that it's critical to make a given component of Sorbet fast. Another key element of why Sorbet is fast is that we deliberately chose a simple type inference algorithm. Specifically, Sorbet only does local type inference, so the result of type checking one method never affects the result of type checking another method. This inference algorithm is a pure function of the code inside a method and Sorbet's immutable indexes of what's defined where. Put this all together, and Sorbet's inference algorithm is embarrassingly parallel, scaling to as many cores as the machine has available while being able to use fast shared memory instead of copying large data structures. A bedrock for engineering values In addition to the productivity boost, an unintentional benefit to come out of adopting Sorbet has been its cultural impact. In a fast-growing company, communicating and codifying cultural norms can be a full time job on its own! Sorbet lends concrete structure to some of Stripe's engineering norms. Consider the cultural norm "Stripe should grow more reliable over time." Despite our best efforts, production incidents happen--our goal when an incident happens is to make sure the same one doesn't happen again. After years of using Sorbet, Stripe engineers reflexively reach for type annotations as a preventative tool when doing incident remediations. As an aside, it's interesting to reflect on the classes of problems that simply don't happen at Stripe anymore (or if they do, they happen exceedingly rarely). For example: typos that used to manifest as NameError: uninitialized constant exceptions in production have been entirely replaced by static type errors. But even some more subtle problems are absent, like this one: def update_invoice(invoice, paid) # ... end update_invoice ('in_1KZ7eP2eZvKYlo2C3B98SLc9', true) # ??? ~ Does this method need to be passed a string invoice ID, or a full invoice object? Scanning the implementation for context clues can sometimes help, but type annotations replace guesswork with machine-checked assurances: sig {params(invoice: Invoice, paid: T::Boolean).void} def update_invoice(invoice, paid) # ... end # error: Expected `Invoice` but found `String` for argument `invoice` update_invoice ('in_1KZ7eP2eZvKYlo2C3B98SLc9', true) ~ View in the Sorbet Playground - This brings up another norm: "public interfaces should have up-to-date documentation." For this we use a clever trick about how Sorbet's strictness levels work. Sorbet activates in files with # typed: true comments at the top of the file, but only in a "best-effort" mode: type annotations aren't required and all methods behave as though their arguments were annotated with T.untyped. But by trading up to # typed: strict, Sorbet stops assuming T.untyped and instead requires signatures for all methods. To encourage this, Stripe's continuous integration (CI) system looks through all code changes and leaves a "Stripe code quality score" in a comment on the pull request, like this one: Blog > Sorbet > Code quality image The score is reported as a weighted sum of signals, where a smaller score is better. There are a lot of inputs to the score, and we hide the ones that don't change in a given pull request, but the one relevant to Sorbet in the picture above reads, "Number of non-test files which are not strictly typed (typed below strict)." This means both the author and reviewer get a heads up when new files aren't using # typed: strict, reminding them that at Stripe we really prefer all Ruby code to be type-annotated. After almost 4 years of Sorbet at Stripe, 85% of all non-test files opt into # typed: strict (and for that matter, over 95% of all files are # typed: true). We often say that most of Stripe's engineers haven't been hired yet. Tooling like Sorbet encodes lessons learned over the years and helps teach these lessons to new engineers in a hands-on environment. As we continue to grow, especially distributed around the globe, Sorbet will continue to serve as a concrete reference point for new and old coworkers to align on shared engineering values. Ruby fits in alongside a handful of other languages in use at Stripe. Stripe is also deeply investing in building new product backends in Java, building delightful frontend experiences with TypeScript, and various pieces of infrastructure in Go. Stripe commits to staffing high quality development experiences across all of these languages, not just Ruby. Making strategic investments in tooling ensures engineers at Stripe write code that is safe and fast as we scale. After all this time, Sorbet is still gaining features, performance improvements, and bug fixes. We love that Sorbet lets us enhance Ruby's natural productivity while helping shape Stripe's code to be resilient and understandable as we grow. As we approach 5 years since Sorbet's conception, we can't wait to see where the next 5 years will lead! Sorbet is written in C++ and compiles to WebAssembly, which means you can try it out in your browser. Below you'll find a link to the Sorbet Playground. Get started with Sorbet Our docs guide you through the process of adding Sorbet to your codebase. Get started Try Sorbet online Play around with Sorbet's type system and editor features, right in your browser. Try Sorbet Discover the Sorbet community Come chat with other Sorbet users, working on large and small codebases alike. Come chat Footnotes 1. RDL takes a unique approach to type checking Ruby: instead of being a purely static system, it waits until all Ruby code has been loaded into memory at runtime and then uses runtime reflection to learn what has been defined. Even back in 2017, loading all of Stripe's code into memory in a single process would have taken multiple minutes. We knew we could type check Stripe's codebase in seconds with a purely static approach. 2. TypedRuby initially architected itself as a more traditional type system, with minimal support for control-flow sensitive type analysis. Unfortunately, control-flow sensitivity tends to be the killer feature in static type systems for dynamically typed languages, as otherwise most idiomatic code fails to type check. TypedRuby's core inference algorithm would likely have needed to be overhauled to implement control-flow sensitive analysis properly. Stay connected Subscribe to receive new blog posts from Stripe in your RSS reader. Subscribe to RSS Like this post? Join our team. Stripe builds financial tools and economic infrastructure for the internet. View roles Have any feedback or questions? We'd love to hear from you. Contact us United States Sign up instantly * AU Australia * AT Austria * BE Belgium * BR Brazil * BG Bulgaria * CA Canada * CY Cyprus * CZ Czech Republic * DK Denmark * EE Estonia * FI Finland * FR France * DE Germany * GR Greece * HK Hong Kong * HU Hungary * IN India * IE Ireland * IT Italy * JP Japan * LV Latvia * LT Lithuania * LU Luxembourg * MY Malaysia * MT Malta * MX Mexico * NL Netherlands * NZ New Zealand * NO Norway * PL Poland * PT Portugal * RO Romania * SG Singapore * SK Slovakia * SI Slovenia * ES Spain * SE Sweden * CH Switzerland * AE United Arab Emirates * GB United Kingdom * United States Request an invite English (United States) * Deutsch * English (United States) * English (United Kingdom) * Espanol (America Latina) * Espanol (Espana) * Francais (France) * Francais (Canada) * Bahasa Indonesia * Italiano * Ri Ben Yu * Nederlands * Portugues (Brasil) * Svenska * aithy * Jian Ti Zhong Wen * (c) 2022 Stripe, Inc. Products * Atlas * Billing * Capital * Checkout * Climate * Connect * Corporate Card * Elements * Identity * Issuing * Payments * Payouts * Pricing * Radar * Revenue Recognition * Sigma * Tax * Terminal * Treasury Developers * Documentation * API reference * API status Company * About * Customers * Enterprise * Partners * Jobs * Blog * Newsroom * Sessions Use cases * SaaS * Platforms * Marketplaces * Ecommerce * Creator Economy * Crypto Resources * Support * Contact * Guides * Privacy & terms * Licenses * COVID-19 * Sitemap * Cookie settings