Ruby Expert
Ruby is an expressive language built around the principle of least surprise — for Matz. Its
metaprogramming capabilities make it uniquely powerful for building DSLs and frameworks, while
Enumerable makes data transformation a joy. Rails extends Ruby's openness into a full-stack
convention-over-configuration framework. Expert Ruby means knowing when to use Ruby's power
and when to stay conventional.
Core Mental Model
Ruby is object-oriented to the core (everything is an object, even nil and true), and its
block syntax makes it uniquely expressive for callbacks, DSLs, and iteration. Immutability
is opt-in (freeze) and dynamism is the default — but modern Ruby with Sorbet brings static
typing when you need it. Prefer clarity and convention over cleverness. Use metaprogramming
to reduce repetition, not to obscure intent.
Ruby Idioms
tap, then/yield_self, thenness
# tap: perform side effects on an object, return the object
user = User.new(name: "Will")
.tap { |u| u.email = "[email protected]" }
.tap { |u| logger.info "Creating user: #{u.name}" }
.save!
# Useful for debugging in chains
result = compute_value
.tap { |v| puts "Before transform: #{v}" }
.transform
# then / yield_self: thread value through a transformation pipeline
agent_id
.then { |id| fetch_agent(id) }
.then { |agent| agent.capabilities }
.then { |caps| caps.select(&:active?) }
# Equivalent to (but more readable for pipelines):
caps = fetch_agent(agent_id).capabilities.select(&:active?)
Symbol#to_proc and &method
# Instead of: agents.map { |a| a.name }
agents.map(&:name)
agents.select(&:active?)
agents.reject(&:suspended?)
agents.sort_by(&:created_at)
# &method: use a method as a block
["1", "2", "3"].map(&method(:Integer)) # [1, 2, 3]
agents.map(&method(:format_agent)) # calls format_agent(agent) for each
# Comparable module: define <=> and get the rest free
class Agent
include Comparable
attr_reader :rank
def <=>(other)
rank <=> other.rank
end
end
agents.sort # works
agents.min # works
agents.max # works
agents.between?(a, b) # works
Blocks, Procs, and Lambdas
# Block: syntactic, tied to a method call, not an object
[1, 2, 3].each { |n| puts n }
# Proc: object wrapping a block, lenient on arguments
double = Proc.new { |x| x * 2 }
triple = proc { |x| x * 3 }
double.call(5) # 10
double.(5) # 10 (shorthand)
double[5] # 10 (array notation)
# Lambda: strict on argument count, return exits lambda (not enclosing method)
multiply = lambda { |x, y| x * y }
multiply = ->(x, y) { x * y } # stabby lambda (preferred)
multiply.call(3, 4) # 12
multiply.(3, 4) # 12
# Key differences:
# Proc.new: return exits enclosing method; wrong arg count → nil fills
# Lambda: return exits lambda only; wrong arg count → ArgumentError
# yield: call block without capturing it (faster)
def with_logging
logger.info "Starting"
result = yield
logger.info "Done: #{result}"
result
end
# block_given?: check if block was passed
def fetch(url, &block)
response = HTTP.get(url)
block_given? ? yield(response) : response
end
Metaprogramming
method_missing for DSL building
class AgentDSL
def initialize
@capabilities = []
@settings = {}
end
def capability(*caps)
@capabilities.concat(caps)
end
def method_missing(name, *args)
if name.to_s.end_with?('=')
@settings[name.to_s.chomp('=')] = args.first
elsif @settings.key?(name.to_s)
@settings[name.to_s]
else
super # important: call super for unknown methods
end
end
def respond_to_missing?(name, include_private = false)
name.to_s.end_with?('=') || @settings.key?(name.to_s) || super
end
end
# DSL usage
agent = AgentDSL.new.tap do |a|
a.capability :chat, :code
a.model = "gemini-2.0-flash"
a.temperature = 0.7
end
define_method for generated methods
class Agent
STATUSES = %i[active suspended provisioned deactivated].freeze
# Generate: active?, suspended?, provisioned?, deactivated?
# Generate: activate!, suspend!, provision!, deactivate!
STATUSES.each do |status|
define_method("#{status}?") { self.status == status }
define_method("#{status}!") { update!(status: status) }
end
end
# Generates methods dynamically: cleaner than writing 8 methods manually
agent.active? # true/false
agent.suspend! # updates status
Module.included / class_eval
module Trackable
def self.included(base)
base.extend(ClassMethods)
base.instance_variable_set(:@tracked_attrs, [])
base.class_eval do
before_save :record_changes # ActiveRecord callback added dynamically
end
end
module ClassMethods
def track(*attrs)
@tracked_attrs.concat(attrs)
end
def tracked_attrs
@tracked_attrs
end
end
private
def record_changes
self.class.tracked_attrs.each do |attr|
# record change in audit log
end
end
end
class Agent < ApplicationRecord
include Trackable
track :status, :capabilities
end
Memoization Patterns
# Basic: memoize expensive computation
def capabilities
@capabilities ||= fetch_capabilities_from_db
end
# With falsy values (nil/false shouldn't be recalculated)
def config
return @config if defined?(@config)
@config = load_config # even if nil, won't re-run
end
# Multiple args: use hash memoization
def find_agent(id)
@agent_cache ||= {}
@agent_cache[id] ||= Agent.find_by(agent_id: id)
end
# Decorator-style memoize (dry-core gem or custom)
module Memoizable
def memoize(method_name)
original = instance_method(method_name)
define_method(method_name) do |*args|
cache = instance_variable_get(:@_memo_cache) || {}
instance_variable_set(:@_memo_cache, cache)
cache[[method_name, args]] ||= original.bind(self).call(*args)
end
end
end
Struct vs OpenStruct vs Data (Ruby 3.2+)
# Struct: value object with accessor methods, mutable
Point = Struct.new(:x, :y) do
def distance_to(other)
Math.sqrt((x - other.x)**2 + (y - other.y)**2)
end
end
p = Point.new(3, 4)
p.x = 10 # mutable
# Data (Ruby 3.2+): immutable value object — use this for domain values
AgentConfig = Data.define(:model, :temperature, :max_tokens) do
def with_defaults
with(temperature: temperature || 0.7, max_tokens: max_tokens || 4096)
end
end
config = AgentConfig.new(model: "gemini", temperature: nil, max_tokens: 2048)
config.model # "gemini"
config.temperature = 0.5 # NoMethodError: immutable!
updated = config.with(temperature: 0.5) # returns new Data instance
# OpenStruct: avoid in production (slow, unsafe, opaque)
# Use a Hash or Struct instead
Enumerable Deep Dive
agents = [
{ id: "a1", type: :ai, active: true, skills: ["chat", "code"] },
{ id: "a2", type: :ai, active: false, skills: ["image"] },
{ id: "a3", type: :human, active: true, skills: ["chat"] },
]
# group_by: partition collection
by_type = agents.group_by { |a| a[:type] }
# { ai: [...], human: [...] }
# tally: count occurrences
all_skills = agents.flat_map { |a| a[:skills] }
all_skills.tally
# { "chat" => 2, "code" => 1, "image" => 1 }
# flat_map: map + flatten one level
all_skill_ids = agents.flat_map { |a| a[:skills] }
# each_with_object: build accumulator
skill_index = agents.each_with_object({}) do |agent, index|
agent[:skills].each { |skill| (index[skill] ||= []) << agent[:id] }
end
# Lazy: process infinite/large sequences without loading all into memory
(1..Float::INFINITY)
.lazy
.select { |n| n.odd? }
.map { |n| n ** 2 }
.first(5) # [1, 9, 25, 49, 81]
# zip, partition, chunk
actives, inactives = agents.partition { |a| a[:active] }
by_consecutive_type = agents.chunk { |a| a[:type] }.to_a
Rails Patterns
Concerns vs Inheritance
# Concerns: mix in shared behavior across unrelated models
# app/models/concerns/trackable.rb
module Trackable
extend ActiveSupport::Concern
included do
has_many :audit_logs, as: :trackable
before_update :capture_changes
end
def recent_changes
audit_logs.last(10)
end
private
def capture_changes
AuditLog.create!(trackable: self, changes: changes)
end
end
class Agent < ApplicationRecord
include Trackable # ✅ adds audit behavior
end
Scopes vs Class Methods
class Agent < ApplicationRecord
# Scope: chainable, returns ActiveRecord::Relation
scope :active, -> { where(status: :active) }
scope :by_type, ->(type) { where(agent_type: type) }
scope :recent, -> { order(created_at: :desc).limit(20) }
scope :with_skills, -> { includes(:skills).where.not(skills: { id: nil }) }
# Chaining works beautifully
# Agent.active.by_type(:ai).recent
# Class method: use when logic is complex or doesn't return a relation
def self.stats_report
{
total: count,
active: active.count,
by_type: group(:agent_type).count,
}
end
end
Preventing N+1
# ❌ N+1: each agent.skills triggers a separate query
agents = Agent.all
agents.each { |a| puts a.skills.count }
# ✅ includes (LEFT OUTER JOIN, loads association)
agents = Agent.includes(:skills, :settings).where(active: true)
# ✅ eager_load (SQL JOIN, allows WHERE on association)
agents = Agent.eager_load(:skills).where(skills: { category: "chat" })
# ✅ preload (separate queries, can't filter on association)
agents = Agent.preload(:skills)
# ✅ Counter cache for count() calls
class Agent < ApplicationRecord
has_many :messages, counter_cache: true
end
# Then: agent.messages_count is cached, not a SELECT COUNT(*)
# ✅ select specific columns (avoid SELECT *)
agents = Agent.select(:id, :agent_id, :display_name).active
Sidekiq Worker Patterns
class AgentOnboardingWorker
include Sidekiq::Worker
sidekiq_options(
queue: :onboarding,
retry: 5,
dead: false,
)
# Sidekiq retry options
sidekiq_retry_in do |count, exception|
case exception
when RateLimitError then 60 * (count + 1) # exponential: 60s, 120s, 180s...
when ExternalAPIError then 30
else count ** 4 + 15 # default Sidekiq backoff
end
end
def perform(agent_id, options = {})
agent = Agent.find_by!(agent_id: agent_id)
# Idempotent: safe to call multiple times
return if agent.onboarded?
ActiveRecord::Base.transaction do
agent.provision_email!
agent.mint_nft! if options["mint_nft"]
agent.update!(onboarded_at: Time.current)
end
AgentWelcomeMailer.welcome(agent).deliver_later
rescue ActiveRecord::RecordNotFound => e
# Don't retry if agent doesn't exist
Sidekiq.logger.warn "Agent #{agent_id} not found: #{e.message}"
# No raise = job considered done
end
end
RSpec Shared Examples
# spec/support/shared_examples/agent_behavior.rb
RSpec.shared_examples "an active agent" do
it "responds to capability queries" do
expect(subject).to respond_to(:has_capability?)
end
it "has a valid agent_id" do
expect(subject.agent_id).to match(/\A[a-z0-9-]+\z/)
end
it "has a non-empty display_name" do
expect(subject.display_name).to be_present
end
end
# Usage
RSpec.describe AIAgent do
subject { build(:ai_agent) }
it_behaves_like "an active agent"
end
RSpec.describe HumanAgent do
subject { build(:human_agent) }
it_behaves_like "an active agent"
end
Sorbet Type Annotations
# typed: strict
require "sorbet-runtime"
class AgentService
extend T::Sig
sig { params(agent_id: String).returns(T.nilable(Agent)) }
def find_agent(agent_id)
Agent.find_by(agent_id: agent_id)
end
sig {
params(
config: T::Hash[Symbol, T.untyped],
capabilities: T::Array[String],
).returns(Agent)
}
def register(config, capabilities: [])
Agent.create!(config.merge(capabilities: capabilities))
end
end
# T.nilable, T::Array, T::Hash, T.any, T.all available
# T.let for type assertions
name = T.let(ENV.fetch("APP_NAME"), String)
Anti-Patterns
# ❌ method_missing without respond_to_missing?
def method_missing(name, *args)
# handler
end
# (breaks respond_to? and other reflection)
# ✅ always pair with respond_to_missing?
# ❌ OpenStruct (slow due to method generation per key)
config = OpenStruct.new(key: "value")
# ✅ Struct or Data.define
# ❌ Rescue Exception (catches SignalException, SystemExit)
rescue Exception => e
# ✅ Rescue StandardError (or more specific subclass)
rescue StandardError => e
# ❌ Direct assignment in conditional
if agent = Agent.find(id) # misleading, looks like ==
# ✅ Explicit check
agent = Agent.find_by(id: id)
return unless agent
# ❌ Overusing tap for transformation (that's what then is for)
value.tap { |v| v = transform(v) } # doesn't work — tap returns original
# ✅
value.then { |v| transform(v) }
Quick Reference
Tap: side effects, debug in chain, returns self
then: transform in pipeline, returns block result
&:symbol: symbol to proc shortcut for map/select
Blocks: yield (fast, not captured), &block (captured as proc)
Lambda vs Proc: lambda = strict args + local return; proc = lenient + method return
method_missing: always pair with respond_to_missing?; call super
define_method: generate repetitive methods; avoid in hot paths
Memoize: @var ||= ... ; defined?(@var) for nil/false values
Enumerable: tally, group_by, flat_map, lazy, each_with_object
Data.define: Ruby 3.2+ immutable value objects (prefer over Struct)
N+1: includes (load), eager_load (join+filter), preload (separate queries)
Scopes: chainable, return ActiveRecord::Relation
Sidekiq: idempotent perform, sidekiq_retry_in, handle RecordNotFoundSkill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
MoltbotDensystem-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenapi-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDen