Top 100 Ruby on Rails Interview Questions and Answers

This post is the list I wish I had when I started. I have grouped the 100 questions so you can study by theme rather than grinding a flat list. Each answer is the version I would actually say out loud, not a textbook paragraph. Code is included where it helps and I have drawn a few diagrams where the words alone fall short.

Let us get into it.

Ruby Fundamentals

1. What is the difference between a symbol and a string?

A string is a mutable object and every time you write "name" you allocate a fresh object.

A symbol like :name is immutable and interned, so the same symbol points to the same object for the life of the process.

That is why symbols are the natural choice for hash keys and method names. They are cheap to compare because you are comparing object identity not character by character.

"name".object_id == "name".object_id   # => false
:name.object_id == :name.object_id     # => true

2. What does nil?, empty?, blank? and present? each check?

This trips people up because two of them are Ruby and two are Rails.

nil? only returns true when something is actually nil (empty/nothing).

empty? checks if a string or array has nothing in it, like "" or []. But careful: if you call it on nil, your code crashes.

blank? is a Rails helper that’s more forgiving. It returns true for nil, false, empty strings, strings with only spaces like " " and empty arrays or hashes. It never crashes.

present? is just the opposite of blank?. If something is blank, present? is false. If it has real content, present? is true.

Quick way to remember it: use blank? when you want a safe check that handles nil for you and empty? only when you’re sure you’re dealing with a string or array and not nil.

nil.blank?      # => true
"  ".blank?     # => true
[].blank?       # => true
"hi".present?   # => true

3. What is the difference between ==, eql? and equal??

== checks value equality and is the one you use most.

eql? checks value and type, so 1.eql?(1.0) is false while 1 == 1.0 is true.

equal? checks object identity, meaning are these literally the same object in memory. You almost never override equal?.

4. Explain blocks, procs and lambdas.

All three are ways to package a chunk of code.

Blocks

A block is the piece of code you pass to a method using do...end or curly braces. It is not an object you can store in a variable on its own; it is always attached to a method call. Inside the method, you run the block with yield or by capturing it.

[1, 2, 3].each do |n|
  puts n
end

# curly brace form, same thing
[1, 2, 3].each { |n| puts n }

Procs

A proc is a block turned into an actual object. You can store it in a variable, pass it around and call it whenever you want with .call.

my_proc = Proc.new { |name| puts "Hi #{name}" }
my_proc.call("VK")   # => Hi VK

Lambdas

A lambda is a special kind of proc. It looks almost the same and you also call it with .call but it behaves more strictly in two important ways.

my_lambda = ->(name) { puts "Hi #{name}" }
my_lambda.call("VK")   # => Hi VK

The two differences that matter in interviews:

First, argument checking. A lambda is strict about the number of arguments and raises an error if you pass the wrong number. A proc is relaxed and just fills missing arguments with nil and ignores extra ones.

p = proc { |a, b| [a, b] }
p.call(1)          # => [1, nil]   no complaint

l = ->(a, b) { [a, b] }
l.call(1)          # => ArgumentError

Second, how return behaves.

A return inside a lambda returns only from the lambda and the surrounding method keeps running.

A return inside a proc returns from the whole method that the proc lives in, which can surprise you.

def test_proc
  p = proc { return "from proc" }
  p.call
  "end of method"     # never runs, proc's return exits the method
end
test_proc            # => "from proc"

def test_lambda
  l = -> { return "from lambda" }
  l.call
  "end of method"     # this runs, lambda's return only exits the lambda
end
test_lambda          # => "end of method"

A block is code you hand to a method on the spot.

A proc is that code saved as an object and it is easygoing about arguments and return.

A lambda is a stricter proc that acts more like a real method: it checks arguments and its return only exits itself.

5. What is a closure and how does it relate to blocks?

A closure is a function that remembers the environment where it was created, including the local variables in scope at that moment.

Blocks, procs and lambdas in Ruby are all closures. This is why a block can reference a variable defined outside it even after the surrounding method has returned.

def counter
  count = 0
  -> { count += 1 }
end

c = counter
c.call  # => 1
c.call  # => 2

6. What is the difference between include, extend and prepend?

All three are ways to mix a module into a class but they differ in how the methods get added and where they sit in the lookup order.

include adds the module’s methods as instance methods. This is the most common one. After including, every object of the class can call those methods.

extend adds the module’s methods as class methods (also called singleton methods). You call them on the class itself, not on instances.

prepend is like include (it also adds instance methods), but it inserts the module ahead of the class in the lookup chain. So if both the module and the class define a method with the same name, the module’s version wins. This is what makes it useful for wrapping or overriding existing methods.

Here is a quick example showing all three:

module Greet
  def hello
    "hello from module"
  end
end

# include -> instance method
class A
  include Greet
end
A.new.hello   # => "hello from module"

# extend -> class method
class B
  extend Greet
end
B.hello        # => "hello from module"

# prepend -> wins over the class's own method
class C
  prepend Greet
  def hello
    "hello from class"
  end
end
C.new.hello    # => "hello from module"  (module beats the class)

7. What is the Ruby method lookup path?

When you call a method, Ruby walks a chain: prepended modules first, then the class itself, then included modules in reverse order of inclusion, then up to the superclass and so on until it hits BasicObject. If nothing is found, it calls method_missing. You can see the chain with SomeClass.ancestors.

method call
   |
   v
prepended modules -> class -> included modules -> superclass -> ... -> BasicObject
   |
   v
method_missing (if nothing found)

8. What is method_missing and when would you use it?

method_missing is the hook Ruby calls when an object receives a message it has no method for. You can override it to handle dynamic method names, which is how things like ActiveRecord’s old dynamic finders worked. It is powerful and also a great way to make code impossible to debug, so use it sparingly and always pair it with respond_to_missing?.

9. What does attr_accessor actually do?

It is a metaprogramming shortcut that defines a getter and a setter method for an instance variable.

attr_reader defines only the getter, attr_writer only the setter.

Under the hood they just write the boilerplate methods you would otherwise type by hand.

attr_accessor is the standard Rails pattern for “attach temporary, computed data to a model for this response.”

class User < ApplicationRecord
  attr_accessor :show_welcome_banner, :matched_score
end

Now you can do:

user.show_welcome_banner = true
user.matched_score = 92

These values:

  • exist only in Ruby memory for that user object
  • are not columns in the database
  • are not saved by user.save
  • disappear once that object/request is gone

Typical uses:

user.current_plan_name = "Pro"      # computed for display
user.invited_by_campaign = true # temporary response-specific flag
user.search_rank = 4 # search result metadata

It is useful when you want to keep response/view-related data attached to the model object without changing the schema.

One caution: for lots of presentation-only data, a presenter/serializer/view model can be cleaner than adding many attr_accessors directly to an ActiveRecord model.

10. What is duck typing?

If it walks like a duck and quacks like a duck, Ruby treats it like a duck. You do not check an object’s class before calling a method on it. You just call the method and trust that any object responding to it will behave sensibly.

A Rails-style example:

def send_notification(user)
user.email
end

This works for any object that has an email method:

Customer.new.email
Admin.new.email
GuestUser.new.email

They do not need to inherit from the same class.

That is duck typing: behavior over type/class.

This is why Ruby code rarely checks types and instead checks respond_to? when it checks at all.

ActiveRecord and the Database

This is where most interviews spend their time and rightly so. ActiveRecord is where Rails projects live or die in production.

11. What is ActiveRecord?

ActiveRecord is Rails’ implementation of the Active Record pattern, an ORM that maps database tables to Ruby classes and rows to objects.

A class inherits from ApplicationRecord and suddenly you have query methods, validations, associations and persistence without writing SQL by hand.

The convention is that a User model maps to a users table.

12. Explain the N+1 query problem and how to fix it.

This is the single most common performance bug in Rails apps, so expect it.

It happens when you load a collection and then trigger a separate query for each record’s association inside a loop.

One query to fetch the parents, then N queries for the children, hence N+1.

# Bad: 1 query for posts, then 1 per post for its author
Post.all.each { |post| puts post.author.name }

# Good: 2 queries total
Post.includes(:author).each { |post| puts post.author.name }

The fix is eager loading with includes. Here is the shape of the problem:

Without includes:           With includes:
  SELECT * FROM posts         SELECT * FROM posts
  SELECT author WHERE id=1    SELECT * FROM authors WHERE id IN (1,2,3)
  SELECT author WHERE id=2
  SELECT author WHERE id=3
  ...                         2 queries, flat
  N+1 queries, grows

13. What is the difference between includes, preload and eager_load?

All three solve N+1 but differently.

preload always runs separate queries, one per association, using IN clauses.

eager_load uses a single LEFT OUTER JOIN.

includes is the smart one: it picks preload by default but switches to eager_load if you reference the association in a where or order.

If you need to filter on a joined table, force the join with references.

Post.includes(:comments).where(comments: { approved: true }).references(:comments)

14. What is the difference between find, find_by and where?

find looks up by primary key and raises RecordNotFound if nothing matches.

find_by takes arbitrary conditions and returns the first match or nil.

where returns an ActiveRecord::Relation, which is a lazy, chainable collection, even if only one record matches.

User.find(1)                    # raises if missing
User.find_by(email: "a@b.com")  # nil if missing
User.where(active: true)        # relation, lazy

15. What does it mean that ActiveRecord relations are lazy?

A relation does not hit the database when you build it. It waits until you actually need the data, for example when you call each, to_a, first, or count.

This lets you chain where, order and limit across many lines and methods and Rails compiles it all into one query at the end.

The gotcha is that you can accidentally fire the query early by calling something like count in the middle.

16. What is the difference between count, length and size on a relation?

count always runs a SELECT COUNT(*) query.

length loads all records into memory and counts the array.

size is the clever one: if the relation is already loaded it counts in memory, otherwise it runs a count query.

As a rule, use size when you are unsure, count when you only need the number and length only when you already need the records loaded anyway.

17. What are scopes and how do they differ from class methods?

A scope is a named, reusable, chainable query fragment.

Functionally a scope and a class method that returns a relation are nearly identical. The one real difference is that a scope always returns a relation even when the body would return nil, so chaining stays safe. A class method returning nil breaks the chain.

class Post < ApplicationRecord
  scope :published, -> { where(status: "published") }
  scope :recent, -> { order(created_at: :desc) }
end

Post.published.recent  # chains cleanly

18. What is the difference between save, save!, create and update?

save returns true or false and runs validations.

save! raises an exception on failure.

create builds and saves in one step, returning the object whether or not it saved.

update assigns attributes and saves in one call.

The bang versions raise, the plain versions return a boolean or the object.

In controllers I lean on the boolean versions, in scripts and seeds I lean on the bang versions so failures are loud.

19. What are migrations and why are they reversible?

A migration is a versioned instruction for changing the database schema, stored as a Ruby file so it lives in source control alongside your code.

Reversibility matters because it lets you roll back a bad deploy. Most migrations are auto-reversible, but if you write raw SQL or do something Rails cannot invert, you define separate up and down methods.

class AddStatusToPosts < ActiveRecord::Migration[7.1]
  def change
    add_column :posts, :status, :string, default: "draft", null: false
    add_index :posts, :status
  end
end

20. What is the difference between change, up and down in a migration?

change is the modern default where Rails figures out the reverse automatically.

up and down are the explicit pair you write when the reverse is not obvious, for example when a migration transforms data.

If you ever write raw SQL in change, also add a reversible block or split into up and down.

21. Explain database transactions in ActiveRecord.

A transaction wraps a set of database operations so they all succeed or all fail together.

If any statement raises, the whole block rolls back. This is essential when two writes must stay consistent, like transferring money or, in my case, booking a slot and decrementing availability at the same time.

ActiveRecord::Base.transaction do
  account.withdraw(100)
  recipient.deposit(100)
end

22. What are optimistic and pessimistic locking?

Both prevent two processes from clobbering each other’s writes, but they bet differently.

Optimistic locking adds a lock_version column and assumes conflicts are rare. It only checks at save time and raises StaleObjectError if the version changed underneath you.

Pessimistic locking calls lock! or with_lock and holds a database row lock for the duration, assuming conflicts are likely. Optimistic is cheaper, pessimistic is safer under heavy contention.

# Pessimistic: locks the row until the block ends
account.with_lock do
  account.balance -= 100
  account.save!
end

23. What is SKIP LOCKED and when is it useful?

SKIP LOCKED is a database feature where a locking query simply skips over rows that are already locked by another transaction instead of waiting for them.

It is perfect for job queues and slot-grabbing, where many workers compete for rows and you want each to grab the next free one without blocking.

Booking.where(status: "pending").lock("FOR UPDATE SKIP LOCKED").first

24. What are polymorphic associations?

A polymorphic association lets one model belong to more than one other model through a single association.

A Comment that can attach to either a Post or a Photo is the classic example.

The table stores two columns, the foreign key id and a type string naming the parent class.

class Comment < ApplicationRecord
  belongs_to :commentable, polymorphic: true
end

class Post < ApplicationRecord
  has_many :comments, as: :commentable
end

25. What is the difference between has_many :through and has_and_belongs_to_many?

Both model a many-to-many relationship.

Both create many-to-many relationships, but has_many :through uses a real join model, while has_and_belongs_to_many uses only a join table.

has_and_belongs_to_many

Use it when the join table is just connecting two models and has no extra data.

class Student < ApplicationRecord
has_and_belongs_to_many :courses
end

class Course < ApplicationRecord
has_and_belongs_to_many :students
end

You need a join table:

create_table :courses_students, id: false do |t|
t.references :student
t.references :course
end

Usage:

student.courses << course

There is no Enrollment model to work with.

has_many :through

Use it when the relationship itself has data or behavior.

class Student < ApplicationRecord
has_many :enrollments
has_many :courses, through: :enrollments
end

class Course < ApplicationRecord
has_many :enrollments
has_many :students, through: :enrollments
end

class Enrollment < ApplicationRecord
belongs_to :student
belongs_to :course

validates :status, presence: true
end

Now the join table has its own model and can store fields such as:

student_id
course_id
status
enrolled_at
grade
payment_id

Usage:

Enrollment.create!(
student: student,
course: course,
status: "active"
)

26. What does dependent: :destroy do and how is it different from :delete_all?

Both clean up associated records when the parent is removed.

dependent: :destroy loads each child and runs its callbacks and validations, so any before_destroy logic fires.

dependent: :delete_all issues a single bulk DELETE and skips callbacks entirely.

Destroy is safer, delete_all is faster. If your children have their own children to clean up, you need destroy.

27. What are ActiveRecord callbacks and what is the danger with them?

Callbacks are hooks that fire at points in an object’s lifecycle, like before_save, after_create or after_commit.

They are convenient for keeping logic next to the model. The danger is that they fire invisibly on every save, so business logic buried in callbacks becomes hard to follow and hard to test and they can trigger surprise side effects in seeds or bulk updates.

I keep callbacks for genuinely data-level concerns and push real business logic into service objects.

28. What is the difference between after_save and after_commit?

Both are ActiveRecord callbacks that fire after you save a record, but the key difference is when they run relative to the database transaction.

after_save runs inside the transaction, right after the record is written but before the transaction is committed. At this point the changes are not yet permanent. If anything later in the transaction raises an error and causes a rollback, whatever after_save did gets rolled back too (at least the database parts of it).

after_commit runs after the transaction has actually committed, meaning the data is now safely and permanently in the database. If the transaction rolls back, after_commit never runs at all.

Here is the timeline:

BEGIN transaction
   |
   save the record
   |
   after_save fires here   <-- still inside, not yet permanent
   |
COMMIT transaction
   |
   after_commit fires here <-- data is now safely saved

Why the difference actually matters

The problem shows up when your callback does something that touches the outside world, like enqueuing a background job, sending an email, or clearing a cache.

Say you enqueue a job in after_save:

class Order < ApplicationRecord
  after_save :notify_warehouse

  def notify_warehouse
    WarehouseJob.perform_later(id)
  end
end

The danger: the job gets enqueued while still inside the transaction. If the transaction then rolls back, the order is gone from the database, but the job is already in the queue. When a worker picks it up, Order.find(id) fails because that record was never actually saved. You also get a race where the job runs before the commit finishes and can’t find the row yet.

Using after_commit fixes this, because the job only gets enqueued once the record is guaranteed to be in the database:

class Order < ApplicationRecord
  after_commit :notify_warehouse, on: :create

  def notify_warehouse
    WarehouseJob.perform_later(id)
  end
end

29. What is the query cache?

Within a single request, Rails caches the result of identical SQL queries so the second call returns the cached result instead of hitting the database again. It is cleared at the end of the request.

It helps with accidental duplicate queries but it is not a substitute for fixing N+1 problems, since different IN lists count as different queries.

30. How do you avoid loading huge tables into memory?

Use both for processing large ActiveRecord datasets without loading everything into memory.

User.all.each do |user|
# loads all users into memory first
end

Better:

User.find_each do |user|
# processes one user at a time
end

find_each fetches records in batches internally, then yields them one by one.

User.find_each(batch_size: 1000) do |user|
user.recalculate_score!
end

Use find_each when you want to work with individual records.

find_in_batches gives you the whole batch as an array:

User.find_in_batches(batch_size: 1000) do |users|
users.each do |user|
user.recalculate_score!
end
end

Use it when you need to do something per batch, such as bulk work:

User.where(active: false).find_in_batches(batch_size: 500) do |users|
User.where(id: users.map(&:id)).update_all(status: "archived")
end
MethodYieldsBest for
find_eachOne record at a timePer-record logic
find_in_batchesArray of recordsBatch-level processing / bulk operations

The Request Lifecycle, Routing and Controllers

Understanding how a request flows through Rails separates people who use the framework from people who understand it.

31. Walk me through what happens when a request hits a Rails app.

The request first passes through the Rack middleware stack, then the router matches the URL and HTTP verb to a controller action. Rails instantiates the controller, runs its before_actions, executes the action, which usually touches a model and then renders a view or returns JSON. The response travels back out through the middleware and to the browser.

Browser
   |
   v
Nginx (web server) <-- serves static files, handles SSL, load balances
   |
   v
Puma  (App server) <-- runs your Rails code, processes + threads
   |
   v
Rack middleware stack  (sessions, cookies, params parsing, etc.)
   |
   v
Router  (matches verb + path -> controller#action)
   |
   v
Controller  (before_actions -> action -> talks to Model)
   |
   v
View / Serializer  (renders HTML or JSON)
   |
   v
Response back out through middleware -> Browser

32. What is Rack?

Rack is the minimal interface between Ruby application servers (like Puma) and Ruby web frameworks (like Rails).

It defines a simple contract: an app is anything that responds to call(env) and returns a status, headers and body.

Rails itself is a Rack app and middleware are just layers that wrap that call. This is why you can run Rails, Sinatra and others on the same servers.

33. What is middleware and when would you write your own?

Middleware sits in the request pipeline and can inspect or modify requests and responses before and after they reach your app.

Rails ships with a stack handling sessions, cookies and more.

You write your own for cross-cutting concerns that should run for every request regardless of route, like request logging, custom auth headers or rate limiting.

Run rails middleware to see the stack.

34. Explain RESTful routing in Rails.

REST maps HTTP verbs to standard actions on a resource.

resources :posts generates seven routes: index, show, new, create, edit, update and destroy, each tied to a verb and path. The point is convention, so any Rails developer can guess your routes without reading the file.

resources :posts
# GET    /posts          -> index
# POST   /posts          -> create
# GET    /posts/:id      -> show
# PATCH  /posts/:id      -> update
# DELETE /posts/:id      -> destroy

35. What is the difference between resources and resource?

resources (plural) is for collections where you have many records identified by id.

resource (singular) is for a thing where there is only one per context and no id in the URL, like a user’s own profile.

The singular form drops the index route and the id segment.

36. What is the difference between PATCH and PUT?

PUT means replace the entire resource, PATCH means apply a partial update.

Rails defaults the update action to PATCH because in practice you almost always send only the changed fields. Both route to the same update action.

37. What are before_action, after_action and around_action?

These are controller filters.

before_action runs before the action, commonly for authentication or loading a record.

after_action runs after.

around_action wraps the action with a yield in the middle.

A before_action that renders or redirects halts the chain, which is how authentication guards work.

class PostsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_post, only: [:show, :edit, :update, :destroy]

  private

  def set_post
    @post = Post.find(params[:id])
  end
end

38. What are strong parameters and why do they exist?

Strong parameters force you to explicitly whitelist which attributes can be mass-assigned from request params. They exist because of a real security hole: without them, a malicious user could submit extra fields, like admin: true and have them saved.

You require the top-level key and permit the safe attributes.

def post_params
  params.require(:post).permit(:title, :body, :status)
end

39. How does Rails handle sessions?

By default Rails stores the session in a cookie that is signed and encrypted, so the client holds the data but cannot read or tamper with it.

For larger or more sensitive session data you can switch the store to the database or a cache store like Redis. The session is how Rails remembers who is logged in across requests.

40. What is CSRF and how does Rails protect against it?

Cross-Site Request Forgery tricks a logged-in user’s browser into making an unwanted request to your app.

Rails defends with an authenticity token embedded in forms and checked on every non-GET request.

If the token is missing or wrong, the request is rejected.

This is why protect_from_forgery and the csrf_meta_tags helper matter and why API-only apps disable it in favor of token auth.

Example:

You are logged into your bank. Your browser holds a session cookie for mybank.com.

You visit a random site. That site has this hidden form:

<form action="https://mybank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="10000" />
</form>

<script>document.forms[0].submit()</script>

Your browser auto-submits it. Your bank sees your valid session cookie. Transfer goes through. You did nothing — you just visited a page.

Why it worked

The browser automatically sends cookies with every request to a matching domain. The bank cannot tell the request came from an attacker’s page and not from you.

You are logged into bank.com (browser holds bank.com's session cookie)
        |
        v
You visit evil.com
        |
        v
evil.com's page fires a POST to bank.com/transfer
        |
        v
Browser AUTO-ATTACHES bank.com's cookie to that request
   (because the request goes TO bank.com, browser doesn't care who started it)
        |
        v
bank.com sees a valid session cookie -> thinks it's really you -> processes transfer

The attacker never saw or read the cookie. They just tricked your browser into making a request and the browser did what it always does: attached the matching cookie for the destination domain.

How a CSRF token stops it

The bank now embeds a secret random token in every form:

<input type="hidden" name="_csrf" value="x9fK2mQ7..." />

The attacker’s page has no way to read that token — the browser’s same-origin policy blocks it. So the forged form submits without the token, the server rejects it.

Attacker has your cookie → still cannot forge the request. Token stops it.

Views, Assets and the Frontend

41. What is the difference between a partial and a helper?

A partial is a reusable chunk of view template, named with a leading underscore, that you render to avoid duplicating markup.

<%= render "shared/post_card", post: @post %>

<%= render partial: "shared/post_card", collection: @posts, as: :post %>

A helper is a Ruby method available in views for formatting logic, like turning a timestamp into a friendly string. Markup goes in partials, logic goes in helpers.

<span><%= formatted_price(1499) %></span>
<%= status_badge(@order.status) %>

42. What is the difference between render and redirect_to?

render shows a view in the same request. URL does not change.

render :new

Use it when validation fails, so form errors stay available.

redirect_to sends the browser to a new URL/request. URL changes.

redirect_to @user

Use it after successful create/update/delete.

Typical pattern:

if @user.save
redirect_to @user
else
render :new
end

The classic bug is rendering after a failed save but forgetting that a redirect would lose the validation errors or redirecting when you meant to render and losing the form data.

43. What are content_for and yield in layouts?

yield is where Rails inserts the current view inside a layout.

<!-- app/views/layouts/application.html.erb -->
<html>
<body>
<%= yield %>
</body>
</html>

If Rails renders users/show.html.erb, its content appears at yield.

content_for lets a view fill a named section in the layout.

<!-- layout -->
<head>
<title><%= yield :title %></title>
</head>
<!-- users/show.html.erb -->
<% content_for :title, "User Profile" %>

<h1>Vivek</h1>

Result:

<title>User Profile</title>
<h1>Vivek</h1>

So:

content_for = provide content for a named placeholder

yield = placeholder in the layout

44. What is the asset pipeline and what changed with importmap and Propshaft?

The asset pipeline manages CSS, JavaScript and images, handling concatenation, minification and fingerprinting for cache busting.

Older Rails used Sprockets.

Modern Rails leans on Propshaft for asset serving and importmaps for shipping JavaScript without a bundler, so you can use modern JS modules straight from the browser with no Node build step for many apps.

45. What is Turbo and how does it fit with Hotwire?

Hotwire is Rails’ approach to building reactive frontends while keeping most logic on the server.

Turbo is its core:

Turbo Drive speeds up navigation by swapping page bodies instead of full reloads,

Turbo Frames update independent sections of a page and Turbo Streams push targeted DOM changes, often over WebSockets.

Stimulus adds the sprinkles of JavaScript when you genuinely need client-side behavior.

User clicks / submits
        |
        v
Turbo Drive     -> speeds up full-page navigation (default)
Turbo Frames    -> updates one section independently
Turbo Streams   -> pushes precise DOM changes (often live over WebSocket)
        |
        v
Stimulus        -> adds small JS behaviors where the server can't do it

Background Jobs, Caching and Performance

This is where Rails apps grow up. The questions here separate the people who have run something in production from the people who have only built tutorials.

46. What is Active Job and how does it relate to Sidekiq?

Active Job is Rails’ abstraction layer for background work. You write a job once against the Active Job API and it can run on any backend adapter.

Sidekiq is one such backend, a fast, Redis-backed job processor. Active Job gives you a consistent interface, Sidekiq gives you the actual muscle. Many teams skip the abstraction and use the Sidekiq API directly when they need its advanced features.

class WelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

WelcomeEmailJob.perform_later(user.id)

47. Why should you pass IDs to jobs instead of objects?

When a job is enqueued, its arguments are serialized and stored in Redis, then deserialized when the worker picks it up. If you pass a whole object, you serialize a stale snapshot that may be out of date by the time the job runs and you waste space. Passing the id means the job fetches the current record. This is one of the most common review comments I leave.

48. What is the difference between perform_now and perform_later?

perform_now runs the job synchronously in the current process, right now.

perform_later enqueues it to run in the background.

Use perform_now in tests or when you genuinely need the result inline and perform_later for anything that should not block the request.

49. How do you make a background job idempotent and why does it matter?

Idempotent means running it twice has the same effect as running it once. It matters because job systems guarantee at-least-once delivery, so a job can run more than once after a retry or crash. You make it idempotent by checking state before acting, for example checking whether the email was already sent before sending it, often guarded by a unique key or a database flag.

50. Explain the caching layers available in Rails.

There are several.

Page caching serves a whole static page, now extracted to a gem and rarely used.

Action caching caches a controller action’s output.

Fragment caching caches a piece of a view, the workhorse for most apps.

Russian doll caching nests fragment caches so an outer cache reuses inner ones.

Low-level caching with Rails.cache.fetch caches arbitrary values like an expensive query result.

Rails.cache.fetch("user_#{user.id}_stats", expires_in: 1.hour) do
  user.expensive_stats_calculation
end

51. What is Russian doll caching and how does cache key invalidation work?

Russian doll caching nests cache fragments inside each other.

The trick is the cache key: it includes the record and its updated_at timestamp, so when a record changes its key changes and the cache misses naturally. With touch: true on associations, updating a child bumps the parent’s timestamp, invalidating the outer fragment too. You never manually expire, the keys do it for you.

[ outer fragment: post + comments ]
   key: post/42-20240601
   |
   +-- [ inner fragment: comment 1 ]  key: comment/1-...
   +-- [ inner fragment: comment 2 ]  key: comment/2-...

Edit comment 1 -> its key changes -> with touch:true, post.updated_at bumps
-> outer key changes -> only the changed parts re-render

52. What is the difference between memoization and caching?

Memoization stores a computed value in an instance variable so it is calculated once per object lifetime, living only in memory for that request.

Caching stores values in an external store like Redis that survives across requests and processes.

Memoization is the @result ||= expensive_call pattern, caching is Rails.cache.

Watch out for memoizing a nil or false result, since ||= will recompute it.

def total
  @total ||= line_items.sum(&:price)
end

53. How would you debug a slow Rails endpoint?

I start by reproducing it and looking at the logs to see the SQL and the time breakdown between database and view rendering.

An APM like New Relic shows exactly where the time goes. Usually it is N+1 queries, a missing index, or rendering a huge collection.

I confirm with EXPLAIN on the slow query, add an index or eager load and measure again. The discipline is measuring before and after, not guessing.

54. What is the difference between Rails.cache.fetch reading and writing?

fetch does both: it tries to read the key and on a miss it runs the block, stores the result and returns it. This read-through pattern avoids the race where you check existence and then write separately. There are still thundering herd concerns under heavy load, which fetch addresses with options like race_condition_ttl.

55. How do database indexes improve performance and what is the cost?

An index is a sorted data structure that lets the database find rows without scanning the whole table, turning a linear search into a logarithmic one. The cost is that every write must also update the index, so indexes slow inserts and updates and take disk space. You index columns you filter or join on frequently, like foreign keys and you avoid indexing everything.

Security

56. How does Rails prevent SQL injection?

ActiveRecord parameterizes queries when you use the safe forms, so user input is sent separately from the SQL and can never be interpreted as code. The danger is string interpolation into a where clause. Always pass values as parameters.

# Dangerous
User.where("email = '#{params[:email]}'")

# Safe
User.where("email = ?", params[:email])
User.where(email: params[:email])

57. How does Rails prevent XSS?

ERB automatically HTML-escapes any value you output with <%= %>, so user content cannot inject script tags. You only become vulnerable when you explicitly mark content as safe with raw or html_safe. The rule is never trust user input you are about to render as raw HTML and sanitize it if you must allow some markup.

58. What is mass assignment vulnerability?

It is the risk that a user submits parameters you did not intend to be settable, like elevating themselves to admin by adding an admin field to a form post. Strong parameters fix this by requiring an explicit whitelist of permitted attributes, so unexpected fields are silently dropped.

59. How do you store passwords securely in Rails?

Never store plaintext. Use has_secure_password, which leans on bcrypt to hash and salt the password.

Bcrypt is deliberately slow, which makes brute forcing expensive. You store only the hash and authentication compares a freshly hashed attempt against it.

class User < ApplicationRecord
  has_secure_password
end

user.authenticate("submitted_password")  # => user or false

60. How would you implement authentication and authorization and what is the difference?

Authentication is proving who you are, authorization is deciding what you are allowed to do.

For authentication I reach for Devise or, increasingly, the built-in generator in newer Rails.

For authorization I use Pundit or CanCanCan to define policies separate from controllers.

Keeping the two concerns separate keeps the code clean as rules grow.

Testing

61. What is the testing pyramid and how does it apply to Rails?

The pyramid says have many fast unit tests at the base, fewer integration tests in the middle and a small number of slow end-to-end system tests at the top.

In Rails terms that is lots of model and service tests, a moderate number of request specs and a handful of system tests driving a real browser.

Inverting the pyramid, with mostly slow browser tests, gives you a suite that is brittle and takes forever.

62. What is the difference between RSpec and Minitest?

Both are mature test frameworks.

Minitest ships with Rails, is lightweight and reads like plain Ruby.

RSpec is a separate gem with a domain-specific language built around describe, context and it, plus rich matchers.

The choice is largely team preference. I have used both happily, though most teams I have worked with default to RSpec.

63. What is the difference between a mock and a stub?

A stub replaces a method to return a canned value, so you control what a dependency returns without verifying it was called.

A mock also sets an expectation that the method gets called in a particular way and the test fails if it does not.

Stubs are about state, mocks are about behavior. Overusing either couples your tests to implementation details.

# Stub: return a value
allow(payment_gateway).to receive(:charge).and_return(true)

# Mock: assert it was called
expect(payment_gateway).to receive(:charge).with(100)

64. What are fixtures and factories and which do you prefer?

Both create test data.

Fixtures are static YAML files loaded once, fast but rigid and easy to break as the schema changes.

Factories, usually via FactoryBot, build objects in Ruby on demand, more flexible and readable at the cost of speed.

I prefer factories for maintainability, with care taken not to build deep object graphs in every test, which slows the suite.

FactoryBot.define do
  factory :user do
    name { "VK" }
    sequence(:email) { |n| "user#{n}@example.com" }
  end
end

65. What is the difference between a unit, request and system spec?

A unit spec tests one class in isolation, like a model or service.

A request spec exercises a full controller action through the routing and middleware, checking the response without a browser.

A system spec drives a real or headless browser to test the whole stack including JavaScript.

Each costs more to run than the last, which is why the pyramid shape matters.

66. How do you test background jobs?

You can test them at two levels. Test the job’s perform method directly as a unit, calling it with arguments and asserting the effect. Separately, assert that the job gets enqueued where it should, using matchers like have_enqueued_job, with the queue adapter set to test mode so jobs do not actually run.

67. What does it mean for a test to be flaky and how do you fix it?

A flaky test passes sometimes and fails other times without code changes.

Common causes are time dependence, relying on database ordering that is not guaranteed, leaked state between tests or race conditions in asynchronous code.

You fix them by freezing time, adding explicit ordering, ensuring proper cleanup and avoiding real sleeps in favor of waiting on conditions.

Flaky tests erode trust in the whole suite, so I treat them as bugs.

Ruby on Rails Internals and Architecture

68. Explain MVC as Rails implements it.

Model View Controller separates concerns. The model holds data and business rules, the view handles presentation and the controller coordinates between them, handling the request and choosing what to render.

Rails adds its own conventions on top, like the router in front of the controller.

The discipline is keeping each layer focused, which in practice means resisting the urge to stuff logic into controllers.

69. What does “convention over configuration” mean?

Rails makes sensible default decisions so you do not have to configure everything explicitly.

Name a class User and it maps to the users table, put a controller action’s view in the conventional folder and it renders automatically.

You only write configuration when you deviate from the convention. This is why Rails apps look similar and why a new developer can navigate them quickly.

70. What is the difference between a concern and a service object?

A concern shares reusable behavior between models/controllers.

A service object handles one business workflow.

Concern

# app/models/concerns/trackable.rb
module Trackable
extend ActiveSupport::Concern

def track_event
puts "Tracked"
end
end
class User < ApplicationRecord
include Trackable
end

Use a concern when multiple classes need the same methods.

Service object

# app/services/create_order.rb
class CreateOrder
def call(user, product)
Order.create!(user: user, product: product)
Payment.charge(user, product)
Notification.send_order_email(user)
end
end
CreateOrder.new.call(user, product)

Use a service object when one action has multiple steps.

so,

Service object = business process / workflow

Concern = reusable shared behavior

71. What is a “fat model, skinny controller” and is it still good advice?

The original advice was to keep controllers thin by pushing logic down into models. It is half right. Thin controllers are good, but blindly fattening models just moves the mess.

The modern refinement is thin controllers, focused models and service objects or other plain Ruby objects for business processes that do not belong to a single model. The real goal is small objects with single responsibilities.

72. What are Railties and engines?

A Railtie is the hook that lets a gem plug into the Rails initialization process.

An engine is essentially a miniature Rails application packaged as a gem, with its own models, views, controllers and routes, that mounts inside a host app.

Devise is an engine. Engines let you build reusable, self-contained slices of functionality.

73. What happens during Rails boot?

Rails loads boot.rb to set up the load path and Bundler, then application.rb and the chosen environment, then runs initializers in order, establishes the database connection and finally loads the middleware stack and routes.

By the end you have a fully configured Rack app ready to serve requests. Understanding this helps when an initializer ordering issue causes a mysterious boot failure.

74. What is autoloading and how did Zeitwerk change it?

Autoloading lets you reference a constant like User without an explicit require and Rails loads the file for you based on naming conventions.

Zeitwerk, the modern autoloader, made this reliable and thread-safe by strictly mapping file paths to constant names. The practical consequence is that your file and folder names must match the module and class nesting exactly or you get a load error.

75. What is the difference between eager loading and lazy loading of the application code?

This is about your app’s own classes, not database records.

In development Rails lazily loads code so changes show up without a restart.

In production it eager loads all your code at boot, which catches naming errors immediately and improves runtime performance and thread safety.

This is why a bug can hide in development and surface only on the first production deploy.

APIs, Serialization and Modern Rails

76. How do you build an API-only Rails app?

You generate it with rails new myapp --api, which strips out view and asset middleware and slims the controllers down to ActionController::API.

You render JSON instead of HTML, handle authentication with tokens rather than cookie sessions and disable CSRF protection in favor of that token auth.

It keeps the framework lean for serving JSON to a frontend or mobile client.

77. What are the options for serializing JSON in Rails?

The simplest is calling to_json or render json: with only and include options, fine for small payloads.

For structure and reuse there is Jbuilder, which builds JSON in a template.

For performance-conscious APIs there are gems like ActiveModel::Serializers, Blueprinter or the fast alba and oj-backed options.

The choice depends on payload complexity and how much control you need over the shape.

78. How do you version an API?

The common approaches are URL versioning, like /api/v1/posts and header versioning, where the client sends an Accept header naming the version.

URL versioning is the most visible and easiest to debug, which is why most teams pick it.

You namespace controllers under Api::V1 and route accordingly. The goal is letting old clients keep working while you evolve the API.

namespace :api do
  namespace :v1 do
    resources :posts
  end
end

79. How do you handle errors and return proper status codes in an API?

You rescue exceptions centrally, often with rescue_from in a base controller and map each error type to the right HTTP status.

A missing record returns 404, a validation failure returns 422, an auth failure returns 401.

The body carries a consistent error shape so clients can parse it.

Consistency across endpoints is what makes an API pleasant to consume.

rescue_from ActiveRecord::RecordNotFound do |e|
  render json: { error: "Not found" }, status: :not_found
end

80. What is pagination and how do you implement it efficiently?

Pagination splits a large result set into pages so you do not return thousands of records at once.

The classic approach uses limit and offset, via gems like Kaminari or Pagy.

Offset pagination gets slow on deep pages because the database still scans the skipped rows, so for large datasets cursor or keyset pagination which filters by the last seen id, performs far better.

# Keyset pagination, scales better than offset
Post.where("id > ?", last_id).order(:id).limit(20)

Deployment, Operations and Scaling

81. How do you scale a Rails application?

You scale in layers.

First fix the obvious inefficiencies, the N+1 queries and missing indexes.

Then scale horizontally by running more app server processes behind a load balancer, since Rails app servers are largely stateless.

Add caching with Redis, move heavy work to background jobs, add read replicas for the database and reach for a CDN for assets.

Most scaling problems are database problems in disguise.

        Load Balancer
        /     |      \
   Puma 1   Puma 2   Puma 3      (stateless app servers, scale out)
        \     |      /
          Redis (cache + Sidekiq)
              |
        Primary DB ---> Read Replicas
              |
          CDN for assets

82. What is the difference between Puma processes and threads?

Puma runs multiple worker processes, each with multiple threads.

Processes give you true parallelism and isolation but each carries its own memory copy.

Threads within a process share memory and are cheaper, handling concurrent requests during IO waits but Ruby’s global interpreter lock limits CPU-bound parallelism within a process.

You tune the mix based on whether your workload is IO-bound or CPU-bound and how much memory you have.

83. How do you handle database migrations with zero downtime?

The risk is that a migration locks a table or that new code expects a schema the old running code does not.

The technique is multi-step.

To remove a column, first deploy code that stops using it, then drop it in a later deploy.

To add a non-null column, add it nullable with a default, backfill in batches, then add the constraint.

Tools like strong_migrations warn you before you ship something dangerous.

84. What are environment variables and Rails credentials used for?

They keep secrets and per-environment config out of your codebase.

Environment variables

These are values set outside your application, in the operating system or hosting environment and your app reads them at runtime. Nothing secret lives in your code; the code just reads from ENV.

# or with a safe fallback
redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379")

Rails credentials are an encrypted file checked into the repo, decrypted at runtime with a master key kept outside the repo.

Either way, the principle is that API keys and passwords never live in plaintext in version control.

85. How would you diagnose a memory leak in a Rails app?

Memory that grows and never comes back across requests points to a leak, often from objects retained in a class variable or a cache without bounds.

I watch process memory over time, use tools like derailed_benchmarks or memory_profiler to find what is being retained and look for the usual suspects: unbounded caches, large objects held in constants, or a gem with a known issue.

Restarting workers periodically with a tool like puma_worker_killer is a pragmatic stopgap while you find the root cause.

Practical and Behavioural

These are the questions that reveal experience rather than memorization.

86. Tell me about a hard bug you debugged in production.

The honest version of this answer is a story, not a definition. The interviewer wants to see your process: how you reproduced it, how you narrowed it down, what tools you used and what you changed to prevent it recurring. My own go-to story is the booking race condition that pushed me toward the dual-layer locking design because it shows the path from symptom to a real architectural fix.

87. How do you decide between writing a feature in Rails versus reaching for a gem?

I weigh how core the feature is, how well maintained the gem is and the cost of owning the code myself. For commodity concerns like authentication, a battle-tested gem saves time and avoids security mistakes. For anything central to the product’s value, or where a gem only does eighty percent of what I need, I lean toward writing and owning it. Dependency count is a real long-term cost.

88. How do you keep a large Rails codebase maintainable?

Small objects with clear responsibilities, consistent conventions so the code is predictable, a test suite you trust and ruthless attention to the boundaries between modules. I push business logic out of controllers and fat models into service objects, keep an eye on the slowest tests and treat the linter and a tool like RuboCop as a shared standard rather than a suggestion.

89. What is technical debt and how do you manage it?

Technical debt is the accumulated cost of shortcuts taken to ship faster, which slows future work until paid down. The key is that some debt is deliberate and reasonable, taken knowingly to hit a deadline. I manage it by making it visible, writing it down rather than leaving it in someone’s head and paying it down incrementally alongside feature work rather than waiting for a mythical rewrite that never comes.

90. How do you approach code review?

I look for correctness first, then clarity, then style, in that order. I try to ask questions rather than issue commands, since the author often has context I lack. I distinguish between things that must change and things that are preferences and I am explicit about which is which. The goal of review is a better codebase and a stronger team, not proving I am clever.

Rapid Fire Fundamentals

A closing round of quick ones that come up constantly.

91. What is the difference between map, each and collect?

each is for doing something, map is for building a new array and collect is just another name for map.

each iterates over a collection and runs your block for every element but it returns the original collection unchanged. You use it for side effects, like printing or saving, when you don’t need a new array back.

[1, 2, 3].each { |n| puts n }
# prints 1, 2, 3
# => [1, 2, 3]   (returns the original array)

map also runs your block for every element but it collects the return value of each block into a new array and gives you that. The original is untouched. You use it when you want to transform data into something new.

[1, 2, 3].map { |n| n * 2 }
# => [2, 4, 6]   (a brand new array of results)

collect is literally an alias for map. Same method, same behavior, different name. map is more common in modern Ruby but you’ll sometimes see collect in older code. Pick one and stay consistent; most people use map.

[1, 2, 3].collect { |n| n * 2 }   # => [2, 4, 6]  (identical to map)

The key difference in one look

numbers = [1, 2, 3]

result = numbers.each { |n| n * 2 }
# result => [1, 2, 3]   (each ignores the block's return value)

result = numbers.map  { |n| n * 2 }
# result => [2, 4, 6]   (map keeps the block's return value)

Notice each threw away the n * 2 results, while map kept them.

92. What is the difference between select, reject and find?

All three filter a collection using a block that returns true or false, but they differ in what they give back:

select keeps the matches, reject throws them out and find returns just the first match.

select returns a new array of every element for which the block is true. (It’s also available as filter, which is an alias.)

[1, 2, 3, 4, 5].select { |n| n.even? }
# => [2, 4]   (all the elements that matched)

reject is the exact opposite. It returns a new array of every element for which the block is false meaning it drops the matches.

[1, 2, 3, 4, 5].reject { |n| n.even? }
# => [1, 3, 5]   (everything that did NOT match)

So select and reject are complements. Given the same block, together they split the collection into two halves.

find returns only the first single element for which the block is true, not an array. And it stops as soon as it finds one (short-circuits), so it doesn’t scan the rest. If nothing matches, it returns nil. (It’s also available as detect, which is an alias.)

[1, 2, 3, 4, 5].find { |n| n.even? }
# => 2   (just the first match, a single value, not an array)

Seeing them side by side:

nums = [1, 2, 3, 4, 5]

nums.select { |n| n.even? }   # => [2, 4]      all matches, as an array
nums.reject { |n| n.even? }   # => [1, 3, 5]   all non-matches, as an array
nums.find   { |n| n.even? }   # => 2           first match, a single value

93. What does &:method_name mean?

It is shorthand for a block that calls one method on each element.

users.map(&:name) is equivalent to users.map { |u| u.name }.

["a", "b", "c"].map(&:upcase)      # => ["A", "B", "C"]
[1, -2, 3, -4].select(&:positive?)  # => [1, 3]
users.map(&:id)                     # => [1, 2, 3]
[" hi ", " yo "].map(&:strip)       # => ["hi", "yo"]

The & converts the symbol into a proc via Symbol#to_proc. It only works for methods that take no arguments.

94. What is the difference between ||= and =?

a ||= b assigns b to a only if a is currently nil or false.

Plain = always assigns.

The ||= form is the standard memoization and default-value idiom, with the caveat that it recomputes when the value is legitimately nil or false.

95. What is the splat operator?

The splat operator (*) lets a method accept a variable number of arguments by gathering them into an array. It’s how you write methods that can take one argument, five arguments, or none at all, without knowing the count ahead of time.

Single splat * is for gathering arguments

When you put * before a parameter, all the extra positional arguments get bundled into an array:

def greet(*names)
  names.each { |n| puts "Hi #{n}" }
end

greet("A", "B", "C")
# Hi A
# Hi B
# Hi C

greet("A")        # works with one
greet             # works with none, names is just []

Inside the method, names is a normal array.

Splat also works the other way, which is, expanding an array

The same * can unpack an array back into separate arguments when you call a method:

def add(a, b, c)
  a + b + c
end

numbers = [1, 2, 3]
add(*numbers)   # => 6   (the array is spread into a, b, c)

So the splat both collects (in a method definition) and spreads (in a method call).

Double splat ** is for keyword arguments

There’s a two-star version, **, that does the same thing but for keyword arguments (hashes with symbol keys). It gathers any extra named arguments into a hash:

def config(**options)
  options.each { |key, value| puts "#{key}: #{value}" }
end

config(host: "localhost", port: 3000)
# host: localhost
# port: 3000

And like the single splat, it can also spread a hash into keyword arguments:

settings = { host: "localhost", port: 3000 }
config(**settings)   # spreads the hash as keyword args

You can mix them

A single method can combine regular parameters, a splat, and a double splat. The order matters: normal args first, then *, then **.

def log(level, *messages, **metadata)
  puts "[#{level}] #{messages.join(' ')} #{metadata}"
end

log(:info, "server", "started", time: "10:00", pid: 42)
# [info] server started {:time=>"10:00", :pid=>42}

Here level grabs the first argument, messages splats up the middle positional ones, and metadata double-splats the keyword ones.

Quick summary:

*   single splat  -> positional args  <-> array
**  double splat  -> keyword args     <-> hash

In a method definition: it gathers extras
In a method call:       it spreads a collection out

96. What is the difference between puts, print and p?

puts writes its argument followed by a newline and returns nil.

print writes without a newline.

p writes the inspected form, showing quotes and structure and returns the argument, which makes it the most useful for quick debugging.

Seeing the difference:

puts "hello"    # hello        (newline after)
print "hello"   # hello        (no newline, cursor stays on the line)
p "hello"       # "hello"      (note the quotes)

The p quotes matter because they reveal the real type:

puts 5      # 5
p 5         # 5
puts "5"    # 5
p "5"       # "5"   <-- now you can tell it's a string not a number

Also, with arrays:

puts [1, 2, 3]   # prints each on its own line: 1  2  3
p [1, 2, 3]      # [1, 2, 3]   (shows the actual structure)

use puts for normal output, print when you don’t want a newline and p when debugging because it shows the true form and hands the value back to you.

97. What does freeze do?

It makes an object immutable, so any attempt to modify it raises.

It is used for true constants and in newer Ruby with the frozen string literal pragma, for string literals to save memory and prevent accidental mutation.

98. What is the difference between to_s and inspect?

to_s returns a human-readable string, used for display.

inspect returns a developer-readable representation showing the object’s structure used in the console and by p.

Seeing the difference:

"hello".to_s       # => "hello"      (just the text)
"hello".inspect    # => "\"hello\""  (with visible quotes)

[1, 2, 3].to_s     # => "[1, 2, 3]"
nil.to_s           # => ""           (empty string)
nil.inspect        # => "nil"        (you can actually see it's nil)

That nil case is the classic example: to_s on nil gives you an invisible empty string while inspect clearly shows nil, which is why debugging with p (which uses inspect) is so useful.

You override to_s for presentation and inspect for debugging output.

99. What is respond_to? and why is it useful?

respond_to? checks whether an object has a particular method before you try to call it. It returns true or false, so you can ask “can this object do X?” without risking a NoMethodError.

"hello".respond_to?(:upcase)   # => true
"hello".respond_to?(:fly)      # => false

user.respond_to?(:admin?)      # => true only if that method exists

Why it’s useful

The main reason is duck typing. In Ruby, you usually don’t care what class an object is, only whether it can do what you need. respond_to? is the polite way to check capability instead of checking class.

# rigid: checks the exact class
if obj.is_a?(Duck)
  obj.quack
end

# flexible: checks the capability
if obj.respond_to?(:quack)
  obj.quack   # anything that can quack works, not just Ducks
end

The second version is more Ruby-like. It works for any object that responds to quack which keeps your code open to new types you didn’t anticipate.

Common real uses

Safely calling an optional method that may or may not exist:

if record.respond_to?(:archived?) && record.archived?
  skip(record)
end

Handling different object types uniformly:

def describe(thing)
  if thing.respond_to?(:each)
    "a collection with #{thing.count} items"
  else
    "a single value: #{thing}"
  end
end

100. What is the single most important thing you have learned working with Rails?

This is the one I would actually end on. For me it is that the database is almost always the real story. The framework makes it easy to write code that works fine with ten rows and falls over with ten million. Most of the hard, interesting problems I have solved, including the locking system, came down to understanding what the database was really doing underneath the convenient ActiveRecord surface. Respect the database and most of Rails falls into place.

Conclusion

The best preparation is having actually built something and run it under load because then these answers stop being trivia and start being stories. If you do not have that yet, build a small app, deliberately give it an N+1 problem, watch it in the logs and fix it. That single exercise will teach you more than this whole list.

Leave a Reply

Your email address will not be published. Required fields are marked *