Guide
Ruby fundamentals explained
Ruby is a dynamic, object-oriented language designed for programmer happiness — readable syntax, powerful metaprogramming, and a culture of convention over configuration. It powers Shopify, GitHub, Stripe’s early stack, and countless JSON APIs behind Ruby on Rails. Unlike Python’s indentation blocks or PHP’s request-per-process history, Ruby emphasizes blocks, duck typing, and gem-based composition: you reach for a library before writing boilerplate. This guide covers the MRI reference implementation and Global VM Lock (GVL), core syntax and objects, classes and modules, Bundler and the gem ecosystem, Rails versus Sinatra for HTTP services, Active Record and background jobs, a Harbor Commerce webhook API worked example, a language decision table, common pitfalls, and a production checklist alongside our Django and Node.js backend guides.
What Ruby is: MRI, YJIT, and the runtime model
The reference implementation is MRI (Matz’s Ruby Interpreter),
also called CRuby. You invoke it with ruby script.rb or run app servers
like Puma and Unicorn that embed the VM. Ruby 3.x ships YJIT, a
just-in-time compiler that speeds hot loops on supported platforms when enabled via
RUBY_YJIT_ENABLE=1.
Ruby code is interpreted from source at load time (with optional bytecode caching in
recent versions). Everything — integers, strings, classes themselves — is
an object with methods. There are no primitive types separate from
objects; 5 responds to .times { ... }. The
Global VM Lock (GVL) means only one Ruby thread executes Ruby
bytecode at a time per process, so CPU-bound parallelism requires multiple processes
(Puma workers) rather than threads alone. I/O-bound work (database, HTTP clients)
releases the GVL, so threaded Puma still handles concurrent requests efficiently.
Alternative implementations
- JRuby — runs on the JVM with true threading; useful for Java interop in enterprise shops.
- TruffleRuby — GraalVM-based, strong performance on long-running servers.
- mruby — lightweight embeddable subset for IoT and game scripting.
Most new web backends target MRI 3.2+ on Linux with YJIT enabled after benchmarking.
Pin the Ruby version in .ruby-version and your container image.
Syntax essentials: blocks, symbols, and duck typing
Ruby’s signature feature is the block — an anonymous
chunk of code passed to a method with do ... end or
{ ... }. Methods yield to blocks for iteration,
resource cleanup, and DSLs:
orders.each do |order|
puts order.id if order.paid?
end
File.open("ledger.csv") do |file|
file.each_line { |line| process(line) }
end # file closed automatically
Symbols (:pending, :order_id) are
immutable, interned identifiers — cheap hash keys and enum-like constants.
Prefer symbols for internal keys; use strings for user-facing text and JSON wire
formats. Duck typing means objects are accepted if they respond to
the methods you call — #each, #to_json —
without explicit interfaces. That flexibility speeds prototyping but demands tests
and clear contracts at API boundaries.
Variables, constants, and truthiness
- Local variables —
snake_case; instance vars@name; class vars@@count(rare). - Constants —
UPPER_SNAKEorPascalCasefor classes and modules; reassignment warns but is not forbidden. - Truthiness — only
falseandnilare falsy;0and""are truthy (unlike JavaScript). - Safe navigation —
user&.address&.zipreturnsnilinstead of raising when intermediate values are missing.
Classes, modules, and metaprogramming
Define classes with class Order < ApplicationRecord. Ruby supports
single inheritance; share behavior through modules included or
extended into classes. include MyModule adds instance methods;
extend MyModule adds class methods. Rails concerns are modules that
encapsulate validations, scopes, and callbacks.
module Auditable
def audit_log(event)
AuditEntry.create!(record: self, event: event)
end
end
class Shipment
include Auditable
end
Attr readers and writers (attr_accessor :status)
replace boilerplate getters. Keyword arguments (required in Ruby 3
without legacy flags) make APIs explicit:
create_order(sku:, quantity:, customer_id:). Metaprogramming via
define_method, method_missing, and class macros powers
Rails’ has_many and RSpec’s describe blocks
— use sparingly in application code; prefer explicit methods for maintainability.
Gems, Bundler, and project layout
Gems are Ruby packages published to RubyGems.org.
Bundler reads a Gemfile, resolves versions, and writes
Gemfile.lock — commit the lockfile so production matches CI.
Run bundle install locally and bundle exec before commands
to use locked gem versions.
- Rails — full-stack MVC framework with Active Record, Action Mailer, Active Job.
- Sinatra — minimal DSL for small APIs and webhooks when Rails is heavy.
- Sidekiq — Redis-backed background jobs with retries and dead-letter queues.
- RSpec / Minitest — testing frameworks; RSpec’s describe/it style dominates greenfield apps.
- RuboCop — linter and formatter enforcing style and many security cops.
Standard layout: app/models, app/controllers,
config/routes.rb, db/migrate, spec/ or
test/. Keep business logic in plain Ruby objects (service objects,
POROs) rather than fat controllers — easier to test and reuse from jobs.
Rails and Sinatra for HTTP services
Ruby on Rails optimizes for CRUD web apps and JSON APIs with
strong conventions: RESTful routes, Active Record migrations, and
rails generate scaffolding. API-only mode strips views and assets;
enable config.api_only = true for headless backends. Controllers stay
thin; strong parameters whitelist mass assignment:
def order_params
params.require(:order).permit(:sku, :quantity, :customer_id)
end
Sinatra fits webhooks, internal tools, and microservices under a few hundred lines. Define routes inline; mount Rack middleware for auth and logging. Choose Rails when you need migrations, mailers, cable channels, and a large gem ecosystem out of the box; choose Sinatra when boot time and memory footprint matter or the surface area is a handful of endpoints. Compare framework depth in our REST API design guide for pagination, idempotency keys, and error envelopes that apply regardless of framework.
Persistence, Active Record, and queries
Active Record is Rails’ ORM: models map to tables, migrations
version schema, and associations declare has_many,
belongs_to, has_many :through. Validations run before
save; callbacks exist but overuse creates hidden side effects — prefer explicit
service objects for multi-step transactions.
class Order < ApplicationRecord
belongs_to :customer
has_many :line_items, dependent: :destroy
validates :external_id, presence: true, uniqueness: true
scope :paid, -> { where(status: :paid) }
end
Avoid N+1 queries: eager-load with includes(:line_items)
when serializing collections. Use find_each for batch processing large
tables. Index foreign keys and columns in WHERE clauses on
PostgreSQL
(the default production database for Rails). Raw SQL is acceptable for reporting
queries when Active Record becomes awkward — still bind parameters, never
interpolate user input.
Concurrency, jobs, and the GVL in production
Puma runs multiple worker processes, each with threaded request handling. Set worker count from CPU cores and memory budget (each worker loads the full Rails app). Long-running tasks — PDF generation, third-party API calls, email — belong in Active Job backed by Sidekiq or Solid Queue, not inside HTTP request cycles.
- Sidekiq — Redis lists; jobs are Ruby classes with
perform; configure retry limits and dead sets. - Idempotent jobs — use unique job IDs or database locks so webhook retries do not double-charge.
- Clockwork / whenever — cron-style schedulers for nightly reconciliation; run as a separate process.
- Action Cable — WebSockets for live dashboards; scale with Redis adapter and sticky sessions or anycable.
CPU-heavy Ruby (image processing, large JSON parsing) should move to background jobs or native extensions — the GVL prevents true parallel Ruby threads on one process. For extreme throughput, extract hot paths to Go or Rust services while keeping Rails as the orchestration layer.
Worked example: Harbor Commerce webhook API
Harbor Commerce accepts partner orders via signed webhooks. Partners POST JSON when carts convert; the API must verify signatures, persist idempotently, and enqueue fulfillment within 200 ms.
- Route — Rails API-only app:
POST /v1/webhooks/ordersinWebhooks::OrdersController#create; skip CSRF; authenticate via HMAC headerX-Harbor-Signature. - Verification — compute
OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)and compare withsecure_compareto prevent timing attacks. - Idempotency — store
external_idfrom payload with a unique index; return200with existing record if duplicate delivery. - Persistence — transaction wrapping
Order.create!and nestedline_itemson Postgres; status:received. - Async fulfillment — enqueue
FulfillOrderJob.perform_later(order.id)to Sidekiq; job calls warehouse API with retries and exponential backoff. - Observability — Lograge JSON logs with
request_id; StatsD counters for signature failures and duplicate webhooks. - Deploy — Puma behind nginx in
Docker;
DATABASE_URLandREDIS_URLfrom secrets; runrails db:migrateonce per release; horizontal scale with stateless web containers and dedicated Sidekiq workers.
Result: partners get fast 200 responses, duplicate webhooks are harmless, and fulfillment retries survive transient warehouse outages without blocking HTTP threads.
Language decision table
| Need | Prefer Ruby | Consider instead |
|---|---|---|
| Greenfield CRUD API with fast hiring pool | Yes — Rails conventions and gems | Django for Python teams |
| E-commerce or SaaS on Rails ecosystem | Yes — Shopify patterns, Active Record maturity | Node.js if team is TypeScript-only |
| Tiny webhook or internal tool (< 500 LOC) | Yes — Sinatra boots quickly | Flask or Go for minimal images |
| CPU-bound parallel computation | No — GVL limits threads | Go, Rust, or Python with native libs |
| Machine learning training pipelines | No | Python |
| WordPress or shared PHP hosting only | No | PHP |
| Existing Rails monolith with test suite | Yes — incremental extraction beats rewrite | Strangler fig to microservices when metrics justify |
| Hard real-time game server | No | C++, Rust, or dedicated game backends |
Common pitfalls
- Mutating default hash/array arguments — shared mutable defaults leak state across calls; use
def foo(bar = [])withbar = bar.dupor keyword args. - Callback spaghetti in Active Record —
after_savechains that send email, charge cards, and sync search; move to explicit services or jobs. - Skipping
bundle exec— system gems diverge from Gemfile.lock; CI and production behave differently. - SQL injection via string interpolation —
where("id = #{params[:id]}")is unsafe; use bound placeholders or hash conditions. - Loading entire tables —
Order.all.eachon millions of rows; usefind_eachbatches. - Ignoring YJIT and Ruby upgrades — Ruby 2.x is EOL; 3.x brings pattern matching, endless methods, and performance wins.
- Global variables and class variables — hidden coupling across tests; inject dependencies or use thread-local Current attributes sparingly.
- Blocking Puma workers — synchronous HTTP to slow partners; enqueue Sidekiq and return 202 when appropriate.
- Over-metaprogramming — magic macros without docs; the next hire cannot grep for method definitions.
Production checklist
- Pin Ruby 3.2+ in
.ruby-version, Dockerfile, and CI; benchmark YJIT on staging. - Commit
Gemfile.lock; runbundle auditand Brakeman in CI. - Puma worker count tuned to RAM; set
WEB_CONCURRENCYandRAILS_MAX_THREADSexplicitly. - PostgreSQL connection pool size matches Puma threads × workers; use PgBouncer at scale.
- Sidekiq queues monitored; alert on latency and dead job growth.
- Structured JSON logging (Lograge or semantic_logger); correlation IDs from nginx
X-Request-Id. - Health endpoints:
/healthzchecks DB and Redis; readiness separate from liveness. - Secrets via environment or vault — never commit
config/master.keyor credentials YAML. - RSpec or Minitest coverage on webhook signature and idempotency paths.
- Load-test checkout and webhook bursts before partner launches; profile with rack-mini-profiler and bullet gem for N+1.
Key takeaways
- Ruby is a dynamic, object-oriented language where blocks, symbols, and duck typing enable expressive DSLs and rapid API development.
- Bundler and gems manage dependencies; Rails provides full-stack conventions while Sinatra suits small HTTP surfaces.
- Active Record maps models to Postgres; eager-load associations and push slow work to Sidekiq jobs.
- The GVL means scale web tier with Puma workers and offload CPU-heavy work — do not expect threaded parallelism for pure Ruby compute.
- Choose Ruby for Rails-native products, webhook APIs, and teams that value convention; prefer other runtimes for ML, hard real-time, or greenfield teams standardized on TypeScript or Python.
Related reading
- Django fundamentals explained — batteries-included Python framework for comparison
- PostgreSQL fundamentals explained — default Rails persistence and indexing patterns
- REST API design explained — idempotency, pagination, and error contracts for JSON services
- Docker fundamentals explained — Puma and Sidekiq container images for production