Binding Ruby rules for all dexpace projects; extends Airbnb's Ruby Style Guide. Target Ruby 4.0+, frozen string literals everywhere, Sorbet # typed: strict with runtime-checked signatures, rubocop + rubocop-airbnb for tooling. Prioritize correctness, explicitness, simplicity — never cleverness, never nil leaking out of an interface, never metaprogramming for its own sake.
This guide is platform-agnostic. It covers the Ruby language, its object model, and runtime-neutral idioms. Framework concerns (Rails, Sidekiq, gem packaging beyond the basics) layer on top of this guide and never weaken it.
This guide extends and defers to an ordered chain. Where they conflict, the higher authority wins:
- Airbnb Ruby Style Guide — canonical. The primary source of truth for whitespace, method/call shape, conditionals, and naming. Where our guidance collides with it, Airbnb wins, except for the deliberate deviations recorded in the ledger below. Airbnb's guide predates modern Ruby in places; the ledger is where we say so.
- Shopify Ruby Style Guide — a decade of large-scale Ruby. It fills the gaps Airbnb leaves open and supplies the modern taste (shorthand hash keys, double quotes,
Hash#fetch, Minitest, squiggly heredocs). Defer to it where Airbnb is silent. - The Ruby Style Guide (rubystyle.guide) — Bozhidar Batsov's community guide, the shared foundation both of the above descend from. The base layer; defer to it where the two above are both silent.
rubocop+rubocop-airbnb— the tooling embodiment of the above. Its cop baseline and formatter decisions are final; formatting is a non-discussion.- This guide's overlay — the dexpace layer on top: the Tiger Style discipline (assertion density, bounded everything, no unbounded recursion, zero debt), the method size cap, mandatory Sorbet
# typed: strictwith runtimesigenforcement, frozen-by-default values, andData.definevalue objects with parse-don't-validate constructors.
Correctness > performance > developer experience. This is the root README's ordering; this guide refines "developer experience" into simplicity > expressiveness. When rules conflict, that order decides.
- Correctness comes first because a fast, simple, expressive program that computes the wrong answer is worthless. In Ruby — a dynamic language — correctness is bought, not given: Sorbet's runtime-checked signatures are the first test suite, and chapter 03 is about not letting
nilor a wrong type cross a boundary unchecked. - Performance comes before simplicity because the right architecture is chosen once, at design time, and is expensive to retrofit. Work with the grain of the runtime: YJIT, object shapes, frozen strings, lazy enumerators. Optimize the slowest resource first: network > disk > memory > CPU.
- Simplicity is the simplest approach that accomplishes the goal: no abstraction for its own sake, no metaprogramming, no cleverness. When two correct, fast-enough designs differ, the simpler one wins.
- Expressiveness comes last because Ruby's expressive power is a temptation as much as a gift; clarity is worth nothing if the code is wrong, slow, or needlessly clever.
All four are held to one standard of elegant, well-structured code, enforced through the chapters' rules and exemplars rather than aspired to as a rank in the priority order.
| # | Document | Scope |
|---|---|---|
| 01 | Formatting & Tooling | rubocop-airbnb baseline; Ruby ≥ 4.0 pinned; frozen_string_literal mandatory; 2-space, 100-col, double quotes, trailing commas; srb tc typecheck gate; metric caps |
| 02 | Naming Conventions | snake_case/CamelCase/SCREAMING_SNAKE_CASE, predicate ?, bang ! only paired, no is_/get_, one class per file, effect-verb naming, no magic numbers |
| 03 | Type Safety & Nil Discipline | # typed: strict, sig on every method, T.let/T.cast need a why, T.must banned outside bridges, &., fetch over [], no nil across boundaries, parse don't validate |
| 04 | Variables & Declarations | local scope, no $globals, no @@class vars, freeze constants, ||= init (not booleans), memoization caveats, attr_reader, no redundant self. |
| 05 | Methods | 25-line cap, keyword args over positional/options-hash, guard clauses, one level of abstraction, next over nested blocks, public_send, pure by default, 2+ assertions |
| 06 | Classes & Data Modeling | data + functions; modules of functions over class-method bags; Data.define value objects; composition via mixins; T::Enum/sealed; make illegal states unrepresentable |
| 07 | Ruby Idioms | Enumerable pipelines, &:sym, tap/then, pattern matching case/in, no for, {} vs do..end, no monkey-patching, squiggly heredocs, Time over DateTime |
| 08 | Error Handling | StandardError subclasses, never rescue Exception, no flow-control by exception, implicit begin, no empty/nil/modifier rescue, raise class + message, cause chaining |
| 09 | Concurrency | GVL realities, Mutex + immutable sharing, Ractors for parallelism, Fibers + async, bounded pools/queues, deadline timeouts over Timeout.timeout, documented races |
| 10 | API Design | minimal public surface, keyword args, sig on every public method, accept duck types / return frozen concretes, parse at boundaries, deprecation + semver, API symmetry |
| 11 | Testing | Minitest, test "..." blocks, AAA paragraphs, descriptive assertions, fakes over mocks, property tests, determinism via injected Time, srb tc as the first suite |
| 12 | Module Organization | Zeitwerk autoloading, one class/module per file, nested-module namespacing, no load-time side effects, gem layout, madge-style circular-dependency ban |
| 13 | Resource Management | block form for every closable, ensure cleanup, bounded pools/queues/caches, IO deadlines, no finalizer reliance, deterministic teardown |
| 14 | Documentation | YARD on public API, never restate a sig, why-comments, file/class header, TODO(Name):, no commented-out code, no block comments |
| 15 | Performance | YJIT, object shapes, allocation hygiene, << over +, lazy enumerators, size over count, sub/tr over gsub, measure first, network > disk > memory > CPU |
Security, performance, and git practices are covered in the root-level code style guide. The cross-cutting docs are language-agnostic; this guide adapts them to Ruby.
The root README defines twelve rules for every dexpace project. Here they are in Ruby's vocabulary.
- Data and functions, not objects. Model state as immutable
Data.definevalue objects; group behaviour into modules of functions and small duck-typed interfaces. Reserve a statefulclassfor lifecycle resources — things you open and close. No inheritance for code reuse; mix in a module instead. - Explicit over implicit. Code says what it does at the call site and does nothing it did not say. No
method_missing, nodefine_methodmagic, no monkey-patching. Every dependency is a parameter, visible in the signature. Asigmakes the type contract explicit;Hash#fetchmakes a missing key explicit. Library options follow their documented defaults; callers pass only what differs, through keyword arguments. - Immutable by default.
frozen_string_literal: truein every file,freezeon every constant,Data.defineover mutableStruct, frozen collections in public signatures. Build a new value by transformation, never by mutation. Mutability is the choice you have to type — that's the right way around. - Errors are values, handled explicitly. Typed
StandardErrorsubclasses per domain, raised with a class and message, chained throughcauseon rethrow. Never rescueException; never an empty rescue, arescue nil, or a modifier rescue that swallows.rescue => errorthen handle — no silent discards. - Composition over inheritance.
<is for narrow, Liskov-clean hierarchies andStandardErrortrees, nothing else. Closed polymorphism is aT::Enumor acase/in; code reuse is a mixed-in module. Small interfaces composed together, never a deep class tree. - Transform, don't mutate. Build pipelines from
map/select/reduce; reach foreachonly for effects or early exit. Methods take input and return new output. Avoid mutating arguments. State changes are explicit, localized, and named. - Always say why. Comments and YARD explain reasoning, not mechanics; a
sigalready states the types, so prose never restates them. Enforcement notes in the guides name their cop. If you can't say why a line exists, question whether it should. - Assert aggressively. A runtime-checked
sigis an assertion on every argument and return. Add explicit preconditions and postconditions withraiseon top: minimum two per method on average. Split compound assertions; assert positive and negative space; pair-assert a property two independent ways. - Limits on everything. Methods cap at 25 lines (cop-enforced), nesting at three levels, params at four. Bound every loop, queue, retry, pool, cache, and fan-out. Timeouts are mandatory on external I/O. No unbounded recursion in library code — all execution provably bounded.
- Small functions, breathing room. Aim for 5–15 lines, one level of abstraction each. Guard clauses first so the happy path stays flush left. Separate logical sections with blank lines — whitespace is free, cramped code is unreadable.
- Performance from the outset. Design-time is when 1000× improvements are cheap. Work with the grain of the runtime — stable object shapes, YJIT, frozen strings, lazy enumerators. Batch over per-row work. Optimize the slowest resource first: network > disk > memory > CPU.
- Zero technical debt. What exists meets the design goals. Perfection over technical debt — debt never gets paid. Do it right the first time; the second chance may never come.
This guide takes Airbnb's Ruby Style Guide as canonical. The entries below are genuine deviations — places we override Airbnb, mostly because its guide predates modern Ruby — plus additions Airbnb does not address. Each is recorded so it can be revisited surgically.
| Rule | Upstream position | Our position | Why |
|---|---|---|---|
| Hash key syntax | Airbnb: hash-rocket symbol keys, { :one => 1 } |
Shorthand symbol keys, { one: 1 }; hash-rockets only when a key is not a symbol |
Airbnb predates ubiquitous 1.9 syntax; shorthand is the modern community and Shopify default. See 07-ruby-idioms.md. |
| String quotes | Airbnb: unspecified / mixed | Double quotes everywhere | One rule, no decision per literal; matches Shopify. See 01-formatting-and-tooling.md. |
| Word arrays | Airbnb: "use %w freely" |
Prefer plain literal arrays of double-quoted strings | Explicit and grep-able; %w is a second string syntax to learn. See 07-ruby-idioms.md. |
| Collection method names | Airbnb: prefer detect over find, reduce over inject |
find, select, reduce, map |
The community/Shopify canonical set; detect was an ActiveRecord-disambiguation that no longer earns its keep. See 07-ruby-idioms.md. |
| Class methods | Airbnb: def self.method |
Grouped in a single class << self block |
Keeps private working for class methods and collects them in one place; matches Shopify. See 06-classes-and-data-modeling.md. |
| Static typing | Not addressed | Mandatory Sorbet # typed: strict with runtime sig checking |
Owner decision; maximum safety in a dynamic language — static and runtime enforcement. See 03-type-safety-and-nil-discipline.md. |
| Method size | No upstream cap | 25-line hard cap, cop-enforced; aim 5–15 | Owner decision; Tiger Style discipline, the Ruby-scaled sibling of Go's 70 and Kotlin's 60. See 05-methods.md. |
| Test framework | Airbnb: unspecified | Minitest | One framework, the Shopify and core-Ruby default. See 11-testing.md. |
- Airbnb Ruby Style Guide — canonical authority. Where our guidance collides, Airbnb wins (save the deviations above).
- Shopify Ruby Style Guide — large-scale modern taste; fills the gaps Airbnb leaves open.
- The Ruby Style Guide (rubystyle.guide) — the community foundation both descend from.
- Sorbet — the type system; runtime-checked signatures as the first test suite.
- RuboCop — the single lint and format baseline for this guide.
- TigerBeetle Tiger Style — assertion density, the method size cap, limits on everything, no unbounded recursion, zero technical debt.
When adopting a new rule or migrating away from a deprecated pattern, apply the change at the file / module level or larger — never mix two styles within the same file. A half-migrated file is more confusing than either end state.