Stack conventions
Ruby on Rails
Ruby on Rails 7/8 applications following the Rails way: Active Record, Hotwire, background jobs and built-in testing.
Ruby on Rails 7/8 applications following the Rails way: Active Record, Hotwire, background jobs and built-in testing.
Used for: rails.
Principles
- Follow Rails conventions over configuration; deviate only with a recorded reason, and match what the application already does.
- Keep controllers thin (params, authorisation, one call, render); put domain behaviour in models, concerns or plain Ruby objects under app/models.
- Whitelist input with strong parameters (params.expect in Rails 8, require/permit before); never mass-assign raw params.
- Load associations deliberately; prevent N+1 queries with includes/preload/eager_load and strict_loading.
- Treat every schema change as a reversible, deploy-safe migration; never edit a migration that has run in shared environments.
- Make background jobs idempotent and pass identifiers (or GlobalID records), not large serialised objects.
- Let the framework protect you (CSRF tokens, output escaping, parameterised queries) and never bypass it without review.
- Test at the right level - model and request tests first, system tests for critical user journeys.
- 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.
Conventions
Accessibility
- Use semantic HTML in views and Rails form helpers (label_tag/form.label) so inputs have labels.
- Show validation errors next to fields and announce them (aria-describedby, role="alert" for summaries).
- Keep Turbo navigation accessible - manage focus after Turbo Stream updates and preserve keyboard navigation.
Concurrency
- Jobs must be idempotent and safe to retry; use database constraints (unique indexes) as the final guard against duplicates.
- Use with_lock or optimistic locking (lock_version) for concurrent updates of the same record.
- Enqueue jobs after the transaction commits so workers never see uncommitted data.
- 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
- Prefer Rails built-ins (Active Storage, Action Mailer, Solid Queue/Cache/Cable, Turbo, Stimulus) before adding gems.
- Upgrade Rails one minor version at a time with deprecation warnings fixed first; run bin/rails app:update.
- Lock gem versions in Gemfile.lock; review new gems for maintenance and security.
- 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
- Keep README setup (bin/setup, bin/dev) accurate; document non-obvious domain rules on the model.
- Annotate public JSON APIs (OpenAPI via rswag or similar if the app has it).
- 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
- Use rescue_from in ApplicationController for known errors mapped to responses; let unknown errors reach the error reporter.
- Prefer save/update returning false for user-input validation and bang methods (save!, create!) inside transactions and jobs where failure is exceptional.
- Report handled errors with Rails.error.report (Rails 7+) so the error reporter sees them.
- 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 Rails.logger with tagged logging (request_id); filter sensitive parameters via config.filter_parameters.
- Never log full params, tokens or personal data; extend filter_parameters when adding sensitive attributes.
- Instrument custom work with ActiveSupport::Notifications where metrics matter.
- 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
- Models singular (Order), tables plural (orders), controllers plural (OrdersController), join tables per Rails defaults.
- RESTful routes with resources; add member/collection routes sparingly and prefer new controllers for new nouns.
- Migrations named for the change (AddStatusToOrders), generated with bin/rails generate migration.
- 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
- Eliminate N+1 with includes/preload; enable strict_loading on associations or models in development and test.
- Add database indexes for foreign keys and frequently filtered columns in the same migration that adds them.
- Use find_each/in_batches for large iterations; select only needed columns; use exists? rather than present? on relations.
- Cache with fragment caching and Rails.cache (Solid Cache or Redis); use counter_cache for counts.
- 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
- Keep protect_from_forgery (default) for cookie sessions; API-only controllers authenticate with tokens instead.
- Never interpolate user input into SQL (where("name = '#{x}'")); use hash conditions or placeholders (where(name: x) or where("name = ?", x)).
- Never call html_safe or raw on user content; rely on ERB escaping and sanitize with an allowlist when HTML is required.
- Authorise every action and scope queries to the current user or tenant (current_user.orders.find(id), not Order.find(id)).
- Run Brakeman on every change; keep force_ssl and secure cookie settings on in production.
- Use has_secure_password or the authentication generator (Rails 8) rather than custom password storage.
- 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.
Structure
- Respect the standard app/ layout; plain Ruby domain objects can live under app/models; add app/services etc. only if the app already uses it.
- Settings per environment in config/environments/*.rb; secrets in encrypted credentials (bin/rails credentials:edit) or environment variables, never committed in plain text.
- Use partials, helpers and ViewComponent (if present) to keep views simple; no queries in views.
- Keep business logic out of callbacks that trigger external side effects.
- 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
- Use the test framework the app already uses (Minitest by default, RSpec if spec/ exists); match its style.
- Model tests for validations and domain methods; request (integration) tests for controllers and APIs; system tests (Capybara) for critical flows only.
- Use fixtures (Rails default) or factory_bot consistently with the existing suite; keep test data minimal.
- Assert queued jobs and sent mail with ActiveJob::TestHelper and ActionMailer test helpers; do not perform real network calls.
- Add a regression test that would catch an N+1 (strict_loading in tests or the bullet/prosopite gem) for list endpoints.
- Unit-test models, jobs and service objects; name tests after the behaviour (
test "rejects an order without lines"). - Done means tested: new or changed behaviour (a model rule, an endpoint, a job) ships with tests in the same change.
- RSpec - describe the class or method ("#method" for instance, ".method" for class methods), context starts with when/with/without, it describes behaviour.
Review checklist
- Are parameters permitted through strong parameters (params.expect or require/permit)?
- Is every record lookup scoped to the current user or tenant and authorised?
- Are associations used in lists eager loaded (no N+1), and do new foreign keys have indexes?
- Is the migration reversible and safe to run while the old code is still serving traffic?
- Are background jobs idempotent, enqueued after commit, with explicit retry/discard rules?
- Is SQL built only with query methods or placeholders, with no string interpolation of input?
- Is user content escaped (no html_safe/raw on input) and is CSRF protection intact?
- Are callbacks free of external side effects?
- Is the controller thin and does new domain logic live in a model or a justified service/form object?
- Do new or changed endpoints have request tests, and models model tests, in the app's test framework?
- Are sensitive attributes added to filter_parameters and kept out of logs?
- Does Brakeman report no new warnings?
Quality checks (run when your repository has the tool configured)
| Check | Kind | Tool | Blocks the pull request |
|---|---|---|---|
brakeman |
security | brakeman | no |
zeitwerk-check |
static | rails zeitwerk | no |
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 |