Ruby and Ruby on Rails are rich, expressive, and powerful technologies that make web development both elegant and productive. In this post, we’ll explore some critical concepts that developers often encounter, along with detailed explanations, advantages, disadvantages, and real-world Rails examples.
1. Garbage Collection (GC) in Ruby
Ruby’s VM uses a mark‑and‑sweep collector with generational enhancements to reduce pause times.
How it works
- Generational Division: Objects are split into young (eden/survivor) and old generations. Young objects are collected more frequently.
- Mark Phase: It traverses from root nodes (globals, stack, constants) marking reachable objects.
- Sweep Phase: Clears unmarked (garbage) objects.
- Compaction (in newer versions): Optionally compacts memory to reduce fragmentation.
# Trigger a minor GC (young generation)
GC.start(full_mark: false)
# Trigger a major GC (both generations)
GC.start(full_mark: true)
Benefits
- Automatic memory management: Developers focus on logic, not free/delete calls.
- Generational optimizations: Short‑lived objects reclaimed quickly, improving throughput.
Drawbacks
- Pause times: Full GC can cause latency spikes.
- Tuning complexity: Advanced apps may require tuning GC parameters (e.g.,
RUBY_GC_HEAP_GROWTH_FACTOR).
Rails Example
Large Rails apps (e.g., Sidekiq workers) monitor GC.stat to detect memory bloat:
stats = GC.stat
puts "Allocated objects: #{stats[:total_allocated_objects]}"
2. ActiveRecord: joins, preload, includes, eager_load
ActiveRecord provides tools to fetch associations efficiently and avoid the N+1 query problem.
| Method | SQL Generated | Behavior | Pros | Cons |
|---|---|---|---|---|
joins | INNER JOIN | Filters by associated table | Efficient filtering; single query | Doesn’t load associated objects fully |
preload | 2 separate queries | Loads parent then child separately | Avoids N+1; simple to use | Two queries; might fetch unnecessary data |
includes | JOIN or 2 queries | Auto‑decides between JOIN or preload | Flexible; avoids N+1 automatically | Harder to predict SQL; can generate large JOINs |
eager_load | LEFT OUTER JOIN | Forces single JOIN query | Always one query with data | Large result sets; potential data duplication |
Examples
# joins: Filter variants with women category products
> ProductVariant.joins(:product).where(product: {category: 'women'})
ProductVariant Load (3.4ms) SELECT "product_variants".* FROM "product_variants" INNER JOIN "products" "product" ON "product"."id" = "product_variants"."product_id" WHERE "product"."category" = 'women'
# preload: Load variants separately
> products = Product.preload(:variants).limit(10)
Product Load (1.4ms) SELECT "products".* FROM "products" /* loading for pp */ LIMIT 10
ProductVariant Load (0.5ms) SELECT "product_variants".* FROM "product_variants" WHERE "product_variants"."product_id" IN (14, 15, 32)
> products.each { |product| product.variants.size}
# includes: Smart loading
products = > Product.includes(:variants).where("category = ?", 'women')
Product Load (1.7ms) SELECT "products".* FROM "products" WHERE (category = 'women') /* loading for pp */ LIMIT 11
ProductVariant Load (0.8ms) SELECT "product_variants".* FROM "product_variants" WHERE "product_variants"."product_id" IN (14, 15)
# eager_load: Always join
Product.eager_load(:variants).where(variants: { stock_quantity: 5 })
> Product.eager_load(:variants).where(variants: { stock_quantity: 5 })
SQL (3.1ms) SELECT DISTINCT "products"."id" FROM "products" LEFT OUTER JOIN "product_variants" "variants" ON "variants"."product_id" = "products"."id" WHERE "variants"."stock_quantity" = 5 LIMIT 11
SQL (1.6ms) SELECT "products"."id" AS t0_r0, "products"."description" AS t0_r1, "products"."category" AS t0_r2, "products"."created_at" AS t0_r3, "products"."updated_at" AS t0_r4, "products"."name" AS t0_r5, "products"."rating" AS t0_r6, "products"."brand" AS t0_r7, "variants"."id" AS t1_r0, "variants"."product_id" AS t1_r1, "variants"."sku" AS t1_r2, "variants"."mrp" AS t1_r3, "variants"."price" AS t1_r4, "variants"."discount_percent" AS t1_r5, "variants"."size" AS t1_r6, "variants"."color" AS t1_r7, "variants"."stock_quantity" AS t1_r8, "variants"."specs" AS t1_r9, "variants"."created_at" AS t1_r10, "variants"."updated_at" AS t1_r11 FROM "products" LEFT OUTER JOIN "product_variants" "variants" ON "variants"."product_id" = "products"."id" WHERE "variants"."stock_quantity" = 5 AND "products"."id" = 15
When to Use
- joins: Filtering, counting, or conditions across tables.
- preload: You only need associated objects later, with less risk of huge joins.
- includes: Default choice; let AR decide.
- eager_load: Complex filtering on associations in one query.
3. Achieving Multiple Inheritance via Mixins
Ruby uses modules as mixins to simulate multiple inheritance.
Pattern
module Auditable
def audit(message)
puts "Audit: #{message}"
end
end
module Taggable
def tag(*names)
@tags = names
end
end
class Article
include Auditable, Taggable
end
article = Article.new
tag "ruby", "rails"
audit "Created article"
Benefits
- Code reuse: Share behavior across unrelated classes.
- Separation of concerns: Each module encapsulates specific functionality.
Drawbacks
- Method conflicts: Last included module wins; resolve with
Module#prependoralias_method.
Rails Example: Concerns
# app/models/concerns/trackable.rb
module Trackable
extend ActiveSupport::Concern
included do
after_create :track_create
end
def track_create
AnalyticsService.log(self)
end
end
class User < ApplicationRecord
include Trackable
end
4. Thread vs Fiber
Ruby offers preemptive threads and cooperative fibers for concurrency.
| Aspect | Thread | Fiber |
|---|---|---|
| Scheduling | OS-level, preemptive | Ruby-level, manual (Fiber.yield/ resume) |
| Overhead | Higher (context switch cost) | Lower (lightweight) |
| Use Cases | Parallel I/O, CPU-bound (with GVL caveat) | Managing event loops, non-blocking flows |
| GVL Impact | All threads share GIL (Global VM Lock) | Fibers don’t bypass GVL |
Thread Example
threads = 5.times.map do
Thread.new { sleep 1; puts "Done in thread #{Thread.current.object_id}" }
end
threads.each(&:join)
Fiber Example
fiber1 = Fiber.new do
puts "Fiber1 start"
Fiber.yield
puts "Fiber1 resume"
end
fiber2 = Fiber.new do
puts "Fiber2 start"
fiber1.resume
puts "Fiber2 resume"
end
fiber2.resume # orchestrates both fibers
Rails Example: Action Cable
Action Cable uses EventMachine or async fibers to handle multiple WebSocket connections efficiently.
5. Proc vs Lambda
Both are callable objects, but differ in return behavior and argument checks.
| Feature | Proc | Lambda |
|---|---|---|
| Return semantics | return exits enclosing method | return exits lambda only |
| Argument checking | Lenient (extra args discarded) | Strict (ArgumentError on mismatch) |
| Context | Carries method context | More like an anonymous method |
Examples
def demo_proc
p = Proc.new { return "from proc" }
p.call
return "after proc"
end
def demo_lambda
l = -> { return "from lambda" }
l.call
return "after lambda"
end
puts demo_proc # => "from proc"
puts demo_lambda # => "after lambda"
Rails Example: Callbacks
# Using a lambda for a conditional callback
class User < ApplicationRecord
after_save -> { Analytics.track(self) }, if: -> { saved_change_to_email? }
end
6. Exception Handling in Ruby
Ruby’s exception model is dynamic and flexible.
Syntax
begin
risky_operation
rescue SpecificError => e
handle_error(e)
rescue AnotherError
fallback
else
puts "No errors"
ensure
cleanup_resources
end
Benefits
- Granular control: Multiple rescue clauses per exception class.
- Flow control:
rescuecan be used inline (foo rescue nil).
Drawbacks
- Performance: Raising/catching exceptions is costly.
- Overuse: Rescuing
StandardErrorbroadly can hide bugs.
Rails Example: Custom Exceptions
class PaymentError < StandardError; end
def process_payment
raise PaymentError, "Insufficient funds" unless valid_funds?
rescue PaymentError => e
errors.add(:base, e.message)
end
7. Key Ruby on Rails Modules
Rails is modular, each gem serves a purpose:
| Module | Purpose | Benefits |
|---|---|---|
ActiveRecord | ORM: models to DB tables | DRY queries, validations, callbacks |
ActionController | Controllers: request/response cycle | Filters, strong parameters |
ActionView | View templates (ERB, Haml) | Helpers, partials |
ActiveModel | Model conventions for non-DB classes | Validations, callbacks without DB |
ActiveJob | Job framework (sidekiq, resque adapters) | Unified API for background jobs |
ActionMailer | Email composition & delivery | Interceptors, mailer previews |
ActionCable | WebSocket support | Streams, channels |
ActiveStorage | File uploads & CDN integration | Direct uploads, variants |
ActiveSupport | Utility extensions (core extensions, inflections) | Time calculations, i18n, concerns support |
8. Method Visibility: public, protected, private
Visibility controls encapsulation and API design.
| Modifier | Access From | Use Case |
|---|---|---|
public | Everywhere | Public API methods |
private | Same instance only | Helper methods not meant for external use |
protected | Instances of same class or subclasses | Comparison or interaction between related objects |
class Account
def transfer(to, amount)
validate_balance(amount)
to.deposit(amount)
end
private
def validate_balance(amount)
raise "Insufficient" if balance < amount
end
protected
def balance
@balance
end
end
Advantages
- Encapsulation: Hides implementation details.
- Inheritance control: Fine‑grained access for subclasses.
Disadvantages
- Rigidity: Can complicate testing private methods.
- Confusion: Protected rarely used, often misunderstood.
Above Summary
By diving deeper into these core concepts, you’ll gain a solid understanding of Ruby’s internals, ActiveRecord optimizations, module mixins, concurrency strategies, callable objects, exception patterns, Rails modules, and visibility controls. Practice these patterns in your own projects to fully internalize their benefits and trade‑offs.
Other Ruby on Rails Concepts 💡
Now, we’ll explore several foundational topics in Ruby on Rails, complete with detailed explanations, code examples, and a balanced look at advantages and drawbacks.
1. Rack and Middleware
Check our post: https://railsdrop.com/2025/04/07/inside-rails-the-role-of-rack-and-middleware/
What is Rack?
Rack is the Ruby interface between web servers (e.g., Puma, Unicorn) and Ruby web frameworks (Rails, Sinatra). It standardizes how HTTP requests and responses are handled, enabling middleware stacking and pluggable request processing.
Middleware
Rack middleware are modular components that sit in the request/response pipeline. Each piece can inspect, modify, or short-circuit requests before they reach your Rails app, and likewise inspect or modify responses before they go back to the client.
# lib/simple_logger.rb
class SimpleLogger
def initialize(app)
@app = app
end
def call(env)
Rails.logger.info("[Request] #{env['REQUEST_METHOD']} #{env['PATH_INFO']}")
status, headers, response = @app.call(env)
Rails.logger.info("[Response] status=#{status}")
[status, headers, response]
end
end
# config/application.rb
config.middleware.use SimpleLogger
Benefits:
- Cross-cutting concerns (logging, security, caching) can be isolated.
- Easily inserted, removed, or reordered.
Drawbacks:
- Overuse can complicate request flow.
- Harder to trace when many middlewares are chained.
2. The N+1 Query Problem
What is N+1?
Occurs when Rails executes one query to load a collection, then an additional query for each record when accessing an association.
@users = User.all # 1 query
@users.each { |u| u.posts.count } # N additional queries
Total: N+1 queries.
Prevention: use eager loading (includes, preload, eager_load).
@users = User.includes(:posts)
@users.each { |u| u.posts.count } # still 2 queries only
Benefits of Eager Loading:
- Dramatically reduces SQL round-trips.
- Improves response times for collections.
Drawbacks:
- May load unnecessary data if associations aren’t used.
- Can lead to large, complex SQL (especially with
eager_load).
3. Using Concerns
What are Concerns?
Modules under app/models/concerns (or app/controllers/concerns) to extract and share reusable logic.
# app/models/concerns/archivable.rb
module Archivable
extend ActiveSupport::Concern
included do
scope :archived, -> { where(archived: true) }
end
def archive!
update!(archived: true)
end
end
# app/models/post.rb
class Post < ApplicationRecord
include Archivable
end
When to Extract:
- Shared behavior across multiple models/controllers.
- To keep classes focused and under ~200 lines.
Benefits:
- Promotes DRY code.
- Encourages separation of concerns.
Drawbacks:
- Can mask complexity if overused.
- Debugging call stacks may be less straightforward.
4. HABTM vs. Has Many Through
HABTM (has_and_belongs_to_many):
- Simple many-to-many with a join table without a Rails model.
class Post < ApplicationRecord
has_and_belongs_to_many :tags
end
Has Many Through:
- Use when the join table has additional attributes or validations.
class Tagging < ApplicationRecord
belongs_to :post
belongs_to :tag
validates :tagged_by, presence: true
end
class Post < ApplicationRecord
has_many :taggings
has_many :tags, through: :taggings
end
Benefits & Drawbacks:
| Pattern | Benefits | Drawbacks |
|---|---|---|
| HABTM | Minimal setup; fewer files | Cannot store metadata on relationship |
| Has Many Through | Full join model control; validations | More boilerplate; extra join model to maintain |
5. Controller Hooks (Callbacks)
Rails controllers provide before_action, after_action, and around_action callbacks.
class ArticlesController < ApplicationController
before_action :authenticate_user!
before_action :set_article, only: %i[show edit update destroy]
def show; end
private
def set_article
@article = Article.find(params[:id])
end
end
Use Cases:
- Authentication/authorization
- Parameter normalization
- Auditing/logging
Benefits:
- Centralize pre- and post-processing logic.
- Keep actions concise.
Drawbacks:
- Overuse can obscure the action’s core logic.
- Callback order matters and can introduce subtle bugs.
6. STI vs. Polymorphic Associations vs. Ruby Inheritance
| Feature | STI | Polymorphic | Plain Ruby Inheritance |
|---|---|---|---|
| DB Structure | Single table + type column | Separate tables + *_type + *_id | No DB persistence |
| Flexibility | Subclasses share schema | Can link many models to one | Full OOP, no DB ties |
| When to Use | Subtypes with similar attributes | Comments, attachments across models | Pure Ruby services, utilities |
STI Example:
class Vehicle < ApplicationRecord; end
class Car < Vehicle; end
class Truck < Vehicle; end
All in vehicles table, differentiated by type.
Polymorphic Example:
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true
end
class Post < ApplicationRecord
has_many :comments, as: :commentable
end
Benefits & Drawbacks:
- STI: simple table; limited when subclasses diverge on columns.
- Polymorphic: very flexible; harder to enforce foreign-key constraints.
- Ruby Inheritance: best for non-persistent logic; no DB coupling.
7. rescue_from in Rails API Controllers
rescue_from declares exception handlers at the controller (or ApplicationController) level:
class Api::BaseController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :render_not_found
rescue_from ActiveRecord::RecordInvalid, with: :render_unprocessable_entity
private
def render_not_found(e)
render json: { error: e.message }, status: :not_found
end
def render_unprocessable_entity(e)
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
end
end
Benefits:
- Centralized error handling.
- Cleaner action code without repetitive
begin…rescue.
Drawbacks:
- Must carefully order
rescue_fromcalls (first match wins). - Overly broad handlers can mask unexpected bugs.
Summary
This post has covered advanced Rails concepts with practical examples, advantages, and pitfalls. By understanding these patterns, you can write cleaner, more maintainable Rails applications. Feedback and questions are welcome—let’s keep the conversation going!
Happy Rails Understanding! 🚀