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 variablessnake_case; instance vars @name; class vars @@count (rare).
  • ConstantsUPPER_SNAKE or PascalCase for classes and modules; reassignment warns but is not forbidden.
  • Truthiness — only false and nil are falsy; 0 and "" are truthy (unlike JavaScript).
  • Safe navigationuser&.address&.zip returns nil instead 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.

  1. Route — Rails API-only app: POST /v1/webhooks/orders in Webhooks::OrdersController#create; skip CSRF; authenticate via HMAC header X-Harbor-Signature.
  2. Verification — compute OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post) and compare with secure_compare to prevent timing attacks.
  3. Idempotency — store external_id from payload with a unique index; return 200 with existing record if duplicate delivery.
  4. Persistence — transaction wrapping Order.create! and nested line_items on Postgres; status :received.
  5. Async fulfillment — enqueue FulfillOrderJob.perform_later(order.id) to Sidekiq; job calls warehouse API with retries and exponential backoff.
  6. Observability — Lograge JSON logs with request_id; StatsD counters for signature failures and duplicate webhooks.
  7. Deploy — Puma behind nginx in Docker; DATABASE_URL and REDIS_URL from secrets; run rails db:migrate once 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

NeedPrefer RubyConsider instead
Greenfield CRUD API with fast hiring poolYes — Rails conventions and gemsDjango for Python teams
E-commerce or SaaS on Rails ecosystemYes — Shopify patterns, Active Record maturityNode.js if team is TypeScript-only
Tiny webhook or internal tool (< 500 LOC)Yes — Sinatra boots quicklyFlask or Go for minimal images
CPU-bound parallel computationNo — GVL limits threadsGo, Rust, or Python with native libs
Machine learning training pipelinesNoPython
WordPress or shared PHP hosting onlyNoPHP
Existing Rails monolith with test suiteYes — incremental extraction beats rewriteStrangler fig to microservices when metrics justify
Hard real-time game serverNoC++, Rust, or dedicated game backends

Common pitfalls

  • Mutating default hash/array arguments — shared mutable defaults leak state across calls; use def foo(bar = []) with bar = bar.dup or keyword args.
  • Callback spaghetti in Active Recordafter_save chains 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 interpolationwhere("id = #{params[:id]}") is unsafe; use bound placeholders or hash conditions.
  • Loading entire tablesOrder.all.each on millions of rows; use find_each batches.
  • 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; run bundle audit and Brakeman in CI.
  • Puma worker count tuned to RAM; set WEB_CONCURRENCY and RAILS_MAX_THREADS explicitly.
  • 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: /healthz checks DB and Redis; readiness separate from liveness.
  • Secrets via environment or vault — never commit config/master.key or 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