Stack conventions
Ruby
Ruby 3.x gems, scripts and services, styled with RuboCop or Standard and tested with RSpec or Minitest.
Ruby 3.x gems, scripts and services, styled with RuboCop or Standard and tested with RSpec or Minitest.
Used for: ruby.
Principles
- Follow the Ruby Style Guide as enforced by the repository's RuboCop (or Standard) configuration.
- Start every file with the magic comment "# frozen_string_literal: true" unless the repository has opted out; do not mutate string literals.
- Build small objects with one responsibility and intention-revealing public methods; keep the public surface minimal (private by default).
- Prefer composition and plain Ruby objects over deep inheritance and monkey patching; use refinements if core classes must change.
- Rescue specific exceptions, never Exception; define a base error class per library and subclass it.
- Prefer Enumerable (map, select, each_with_object, sum) over manual loops and index variables.
- Make dependencies explicit (keyword arguments, constructor injection) rather than reaching into globals.
- Write behaviour-focused specs; every change ships with tests at the right level.
- Keep methods short and guard with early returns; avoid nesting conditionals more than two levels deep.
Conventions
Accessibility
Concurrency
- Assume code may run under threaded servers (Puma, Sidekiq); avoid mutable class-level state and memoisation on class variables.
- Protect shared state with Mutex or use thread-safe structures (concurrent-ruby) when sharing is unavoidable.
- Use Ractors only for experiments; they remain experimental in current Ruby releases.
Dependencies
- Commit Gemfile.lock for applications; gems declare dependencies in the gemspec with pessimistic constraints (~>).
- Declare the supported Ruby version (required_ruby_version in gemspecs, .ruby-version for applications).
- Group development and test gems; keep the dependency list small and maintained.
Documentation
- Document public APIs with YARD tags (@param, @return, @raise) where the repository uses YARD.
- Ship RBS signatures in sig/ or Sorbet sigs if the repository type-checks; do not mix systems.
- Keep README usage examples runnable; record changes in CHANGELOG.md.
Errors
- Rescue the narrowest exception class; never bare rescue Exception (it catches signals and SystemExit).
- Library errors inherit from a single MyLib::Error < StandardError so callers can rescue the family.
- Do not use exceptions for flow control; do not swallow errors silently (empty rescue).
- Re-raise with context using raise NewError, "message" (the original is kept as cause).
- Use ensure for cleanup, or block-based APIs (File.open with a block) that close automatically.
Logging
- Use the Logger from the standard library (or the framework logger) injected into objects; no puts or p in library code.
- Log structured key=value or JSON; never log credentials, tokens or personal data.
- Pass a block for expensive messages (logger.debug { expensive }).
Naming
- snake_case for methods, variables and file names; CamelCase for classes and modules; SCREAMING_SNAKE_CASE for constants.
- Predicate methods end in ?, dangerous or mutating variants of an existing method end in !.
- One class or module per file, path matching the constant (lib/billing/invoice.rb defines Billing::Invoice) so Zeitwerk can load it.
- Avoid get_/set_ prefixes; use attribute readers and writers.
Performance
- Prefer lazy enumerators for large or infinite streams; avoid building intermediate arrays in hot paths.
- Use frozen string literals and symbols for repeated keys; avoid string concatenation in loops (use << on a mutable buffer or Array#join).
- Profile with stackprof or benchmark-ips before optimising; enable YJIT in production on Ruby 3.3+.
Security
- Never eval, instance_eval with strings, send or constantize user input; whitelist method names.
- Use YAML.safe_load (YAML.load is only safe by default from Psych 4) and JSON.parse, never Marshal.load on untrusted data.
- Use Open3 with an argument array for shell commands, never backticks or system with interpolated strings.
- Run bundler-audit (or bundle audit) to catch vulnerable gems.
- Use SecureRandom for tokens and Rack::Utils.secure_compare or OpenSSL.fixed_length_secure_compare for secret comparisons.
Structure
- Gems follow the bundler gem layout (lib/
.rb, lib/ /, sig/ for RBS, spec/ or test/). - Namespace everything under one top-level module to avoid constant collisions.
- Keep executables thin under exe/ or bin/, delegating to library classes.
- Use keyword arguments for methods with more than two parameters or boolean flags.
Testing
- RSpec - describe the class or method ("#method" for instance, ".method" for class methods), context starts with when/with/without, it describes behaviour.
- One behaviour per example; use let and subject sparingly and lazily; avoid let! unless setup must run.
- Prefer verifying doubles (instance_double, class_double) over plain double so stubs match real interfaces.
- Use shared examples only for real shared contracts; avoid deep nesting of contexts.
- Run tests in random order (config.order = :random) to expose order dependencies; use SimpleCov for coverage visibility.
- Stub HTTP at the boundary (WebMock or VCR); tests never reach the network.
- Unit specs exercise one object with collaborators doubled; integration specs exercise the real collaborators, database and HTTP stack together where the change crosses them.
- Name spec files after the class (
order_spec.rb) and examples after the behaviour (it "rejects an order without lines").
Review checklist
- Does bundle exec rubocop report no new offences against the repository's configuration?
- Does each file start with the frozen_string_literal magic comment (if the repository uses it)?
- Are exceptions rescued narrowly, with a library-level base error class?
- Are classes small with a minimal public interface and private helpers?
- Is there any eval, send, constantize, Marshal.load or shell interpolation on untrusted input?
- Are collaborators injected rather than referenced as globals or hard-coded constants?
- Do the specs describe behaviour, use verifying doubles and avoid the network?
- Are names idiomatic (predicate?, bang!, no get_/set_) and files placed to match constants?
- Is there no puts/p/pp debugging output left in library code?
- Are Gemfile/gemspec changes justified and constrained with ~>?
- Is shared mutable state avoided or synchronised for threaded servers?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
rubocop |
lint | rubocop | no |
standardrb |
lint | standard | no |
bundler-audit |
security | bundler-audit | no |
sorbet |
typecheck | sorbet | no |
steep |
typecheck | steep | no |
gitleaks |
security | gitleaks | no |