Deep Dive into Essential 🛍️ Ruby and Ruby on Rails Concepts

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

  1. Generational Division: Objects are split into young (eden/survivor) and old generations. Young objects are collected more frequently.
  2. Mark Phase: It traverses from root nodes (globals, stack, constants) marking reachable objects.
  3. Sweep Phase: Clears unmarked (garbage) objects.
  4. 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.

MethodSQL GeneratedBehaviorProsCons
joinsINNER JOINFilters by associated tableEfficient filtering; single queryDoesn’t load associated objects fully
preload2 separate queriesLoads parent then child separatelyAvoids N+1; simple to useTwo queries; might fetch unnecessary data
includesJOIN or 2 queriesAuto‑decides between JOIN or preloadFlexible; avoids N+1 automaticallyHarder to predict SQL; can generate large JOINs
eager_loadLEFT OUTER JOINForces single JOIN queryAlways one query with dataLarge 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#prepend or alias_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.

AspectThreadFiber
SchedulingOS-level, preemptiveRuby-level, manual (Fiber.yield/ resume)
OverheadHigher (context switch cost)Lower (lightweight)
Use CasesParallel I/O, CPU-bound (with GVL caveat)Managing event loops, non-blocking flows
GVL ImpactAll 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.

FeatureProcLambda
Return semanticsreturn exits enclosing methodreturn exits lambda only
Argument checkingLenient (extra args discarded)Strict (ArgumentError on mismatch)
ContextCarries method contextMore 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: rescue can be used inline (foo rescue nil).

Drawbacks

  • Performance: Raising/catching exceptions is costly.
  • Overuse: Rescuing StandardError broadly 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:

ModulePurposeBenefits
ActiveRecordORM: models to DB tablesDRY queries, validations, callbacks
ActionControllerControllers: request/response cycleFilters, strong parameters
ActionViewView templates (ERB, Haml)Helpers, partials
ActiveModelModel conventions for non-DB classesValidations, callbacks without DB
ActiveJobJob framework (sidekiq, resque adapters)Unified API for background jobs
ActionMailerEmail composition & deliveryInterceptors, mailer previews
ActionCableWebSocket supportStreams, channels
ActiveStorageFile uploads & CDN integrationDirect uploads, variants
ActiveSupportUtility extensions (core extensions, inflections)Time calculations, i18n, concerns support

8. Method Visibility: public, protected, private

Visibility controls encapsulation and API design.

ModifierAccess FromUse Case
publicEverywherePublic API methods
privateSame instance onlyHelper methods not meant for external use
protectedInstances of same class or subclassesComparison 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:

PatternBenefitsDrawbacks
HABTMMinimal setup; fewer filesCannot store metadata on relationship
Has Many ThroughFull join model control; validationsMore 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

FeatureSTIPolymorphicPlain Ruby Inheritance
DB StructureSingle table + type columnSeparate tables + *_type + *_idNo DB persistence
FlexibilitySubclasses share schemaCan link many models to oneFull OOP, no DB ties
When to UseSubtypes with similar attributesComments, attachments across modelsPure 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_from calls (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! 🚀

Inside Rails: The Role of Rack 🗄 and Middleware 🔌

Rack provides a minimal, modular, and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the bridge between web servers, web frameworks, and web application into a single method call.

Where is it used?

  • Rails (built on Rack)
  • Sinatra and Hanami
  • Middleware development

What is a Rack-Based Application?

A Rack-based application is any Ruby web application that implements the Rack interface. This means the app must follow Rack’s simple calling convention:

app = Proc.new do |env|
  ['200', { 'Content-Type' => 'text/html' }, ['Hello, Rack!']]
end

This returns an array of three elements:

  1. HTTP status code ('200')
  2. Headers ({ 'Content-Type' => 'text/html' })
  3. Response body (['Hello, Rack!'])
Example: Basic Rack Application
require 'rack'

app = Proc.new do |env|
  ['200', { 'Content-Type' => 'text/html' }, ['Hello, Rack!']]
end

Rack::Handler::WEBrick.run app, Port: 9292

Run it with:

ruby my_rack_app.rb

Open http://localhost:9292 in your browser.

Does Rails Use Rack?

Yes, Rails uses Rack. Rack serves as the interface between Rails and web servers like Puma or WEBrick.

How Rails Uses Rack

When a request comes in:

  1. The web server (Puma/WEBrick) receives it.
  2. The server passes the request to Rack.
  3. Rack processes the request and sends it through Rails middleware.
  4. After passing through the middleware stack, Rails’ router (ActionDispatch) decides which controller/action should handle the request.
  5. The response is generated, sent back through Rack, and returned to the web server.

Check /design_studio/config.ru file in our Rails 8 app is responsible for starting the server.

You can actually run a Rails app using just Rack!

  1. Create a config.ru file / use existing one:
require_relative 'config/environment'
run Rails.application
  1. Run it using Rack:
rackup -p 4343

open http://localhost:4343/products

This runs your Rails app without Puma or WEBrick, proving Rails works via Rack.

Is Rack a Server?

No, Rack is not a server. Instead, Rack is a middleware interface that sits between the web server (like Puma or WEBrick) and your Ruby application (like Rails or Sinatra).

How Does Rack Fit with Web Servers Like Puma and WEBrick?

Puma and WEBrick support Rack by implementing the Rack::Handler interface, allowing them to serve any Rack-based application, such as Rails and Sinatra.

  • Puma and WEBrick are not built “on top of” Rack—they are independent web servers.
  • However, they implement Rack::Handler, which means they support Rack applications.
  • This allows them to serve Rails, Sinatra, and other Rack-based applications.

The Relationship Between Rack, Web Servers, and Rails

  1. Rack provides a standard API for handling HTTP requests and responses.
  2. Web servers (Puma, WEBrick, etc.) implement Rack::Handler so they can run any Rack-based app.
  3. Rails supports Rack by implementing the Rack interface, allowing it to interact with web servers and middleware.

How Rails Supports Rack

  1. Rack Middleware: Rails includes middleware components that process requests before they reach controllers.
  2. Rack Interface: Rails applications can be run using config.ru, which follows the Rack convention.
  3. Web Server Communication: Rails works with Rack-compatible servers like Puma and WEBrick.

Illustration of How a Request Flows

  1. The browser sends a request to the server (Puma/WEBrick).
  2. The server passes the request to Rack.
  3. Rack processes the request (passing it through middleware).
  4. Rails handles the request and generates a response.
  5. The response goes back through Rack and is sent to the server, which then passes it to the browser.

So, while Rack is not a server, it allows web servers to communicate with Ruby web applications like Rails.

Adding Middleware in a Rails 8 App

Middleware is a way to process requests before they reach your Rails application.

How Does Middleware Fit In?

Middleware in Rails is just a Rack application that modifies requests/responses before they reach the main Rails app.

Example: Custom Middleware

Create a new file in app/middleware/my_middleware.rb:

class MyMiddleware
  def initialize(app)
    @app = app
  end

  def call(env)
    status, headers, body = @app.call(env)
    body = ["Custom Middleware: "] + body
    [status, headers, body]
  end
end

Now, add it to Rails in config/application.rb:

config.middleware.use MyMiddleware

Restart your Rails server, and all responses will be prefixed with Custom Middleware: