Ruby
Purpose
Write Ruby that stays readable as it grows: small objects with clear responsibilities, controllers that stay thin, and ActiveRecord queries that do not quietly issue a query per row.
When to Use
- Writing or reviewing Ruby and Rails code.
- Refactoring fat models or fat controllers.
- Diagnosing slow endpoints caused by ActiveRecord.
- Structuring an RSpec suite.
- Designing background jobs and their retry semantics.
Capabilities
- Object design: service objects, form objects, query objects, value objects.
- ActiveRecord: eager loading, scopes, batching, index-aware querying.
- Background processing with Sidekiq or Active Job, including idempotency.
- RSpec structure: request specs, model specs, factories, shared examples.
- RuboCop and Sorbet/RBS typing where valuable.
Inputs
- Application source, Gemfile, Rails version.
- Slow-query or profiling data when performance is the concern.
Outputs
- Controllers that authenticate, authorize, delegate, and render — nothing else.
- Named scopes and eager-loaded queries with no N+1.
- Specs that describe behavior rather than restate implementation.
Workflow
- Locate the responsibility — If a method touches HTTP, business rules, and persistence, split it before changing it.
- Extract a service object — One public
call, explicit dependencies, no global state.
- Fix the queries — Add
includes, convert loops into set operations, batch with find_each.
- Make jobs idempotent — Retries are guaranteed; double-execution must be harmless.
- Gate — RSpec, RuboCop, and Bullet (or equivalent) to fail the build on N+1.
Best Practices
- Controllers do not contain business logic. If a controller action exceeds ten lines, extract.
- Callbacks (
after_save, before_create) create action at a distance. Prefer explicit service calls.
- Use
find_each for anything that could exceed a few thousand rows; all.each loads the table into memory.
- Every background job should be safe to run twice. Key the side effect, not the invocation.
rescue => e without re-raising swallows bugs. Rescue the specific class.
- Freeze constants and prefer immutable value objects for domain concepts.
Examples
Service object with explicit failure:
# frozen_string_literal: true
class RefundOrder
Result = Struct.new(:success?, :refund, :error, keyword_init: true)
def initialize(gateway: PaymentGateway.new)
@gateway = gateway
end
def call(order:, amount_cents:)
return failure("order not refundable") unless order.refundable?
return failure("amount exceeds order total") if amount_cents > order.total_cents
refund = @gateway.refund(order.charge_id, amount_cents)
order.update!(refunded_cents: order.refunded_cents + amount_cents)
Result.new(success?: true, refund: refund)
rescue PaymentGateway::Error => e
failure(e.message)
end
private
def failure(message) = Result.new(success?: false, error: message)
end
Notes
- Bullet in the test environment, configured to raise, turns N+1 queries into failing tests.
frozen_string_literal: true is worth adding to every file; string allocation is a real cost in hot paths.
- Sidekiq retries with exponential backoff by default. Set
sidekiq_options retry: explicitly rather than relying on the default for anything that touches money.
1---2name: ruby3description: Use when writing Ruby or working in Rails codebases. Covers idiomatic object design, service objects, ActiveRecord query performance, RSpec structure, and RuboCop-clean style.4---56# Ruby78## Purpose910Write Ruby that stays readable as it grows: small objects with clear responsibilities, controllers that stay thin, and ActiveRecord queries that do not quietly issue a query per row.1112## When to Use1314- Writing or reviewing Ruby and Rails code.15- Refactoring fat models or fat controllers.16- Diagnosing slow endpoints caused by ActiveRecord.17- Structuring an RSpec suite.18- Designing background jobs and their retry semantics.1920## Capabilities2122- Object design: service objects, form objects, query objects, value objects.23- ActiveRecord: eager loading, scopes, batching, index-aware querying.24- Background processing with Sidekiq or Active Job, including idempotency.25- RSpec structure: request specs, model specs, factories, shared examples.26- RuboCop and Sorbet/RBS typing where valuable.2728## Inputs2930- Application source, Gemfile, Rails version.31- Slow-query or profiling data when performance is the concern.3233## Outputs3435- Controllers that authenticate, authorize, delegate, and render — nothing else.36- Named scopes and eager-loaded queries with no N+1.37- Specs that describe behavior rather than restate implementation.3839## Workflow40411. **Locate the responsibility** — If a method touches HTTP, business rules, and persistence, split it before changing it.422. **Extract a service object** — One public `call`, explicit dependencies, no global state.433. **Fix the queries** — Add `includes`, convert loops into set operations, batch with `find_each`.444. **Make jobs idempotent** — Retries are guaranteed; double-execution must be harmless.455. **Gate** — RSpec, RuboCop, and Bullet (or equivalent) to fail the build on N+1.4647## Best Practices4849- Controllers do not contain business logic. If a controller action exceeds ten lines, extract.50- Callbacks (`after_save`, `before_create`) create action at a distance. Prefer explicit service calls.51- Use `find_each` for anything that could exceed a few thousand rows; `all.each` loads the table into memory.52- Every background job should be safe to run twice. Key the side effect, not the invocation.53- `rescue => e` without re-raising swallows bugs. Rescue the specific class.54- Freeze constants and prefer immutable value objects for domain concepts.5556## Examples5758**Service object with explicit failure:**5960```ruby61# frozen_string_literal: true6263class RefundOrder64 Result = Struct.new(:success?, :refund, :error, keyword_init: true)6566 def initialize(gateway: PaymentGateway.new)67 @gateway = gateway68 end6970 def call(order:, amount_cents:)71 return failure("order not refundable") unless order.refundable?72 return failure("amount exceeds order total") if amount_cents > order.total_cents7374 refund = @gateway.refund(order.charge_id, amount_cents)75 order.update!(refunded_cents: order.refunded_cents + amount_cents)76 Result.new(success?: true, refund: refund)77 rescue PaymentGateway::Error => e78 failure(e.message)79 end8081 private8283 def failure(message) = Result.new(success?: false, error: message)84end85```8687## Notes8889- Bullet in the test environment, configured to raise, turns N+1 queries into failing tests.90- `frozen_string_literal: true` is worth adding to every file; string allocation is a real cost in hot paths.91- Sidekiq retries with exponential backoff by default. Set `sidekiq_options retry:` explicitly rather than relying on the default for anything that touches money.