Top 60 Ruby Interview Questions (2026)

The 60 Ruby questions interviewers actually ask, with direct answers, runnable code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.

60 questions with answers

What Is Ruby?

Key Takeaways

  • Ruby is a dynamic, object-oriented, interpreted language designed by Yukihiro Matsumoto to make programmers happy through readable, expressive syntax.
  • Everything in Ruby is an object, including numbers and nil, and blocks plus mixins give it a distinct feel compared with other scripting languages.
  • Interviews test how well you understand its object model, blocks and iterators, and metaprogramming, not just syntax.
  • This page is a question bank: work your tier first, read one tier up, and say answers out loud because delivery is evaluated too.

Ruby is a dynamic, interpreted, object-oriented programming language created by Yukihiro Matsumoto (Matz) and first released in 1995, built around the idea that code should be pleasant to read and write. Everything is an object, even integers and nil, and the language leans on blocks, mixins, and open classes to stay expressive. It's dynamically and strongly typed, which is why '1' + 1 raises instead of coercing silently. Ruby powers Ruby on Rails, and most Ruby hiring runs through Rails work, so interviews probe the object model, blocks and iterators, and metaprogramming rather than memorized trivia. This page collects the 60 questions that come up most, each with a direct answer and runnable code. If you're building fundamentals, the official documentation at docs.ruby-lang.org is the canonical reference; increasingly the first Ruby round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.

60Questions with answers on this page
3Difficulty tiers: freshers, intermediate, experienced
50+Runnable code snippets you can practice from
45-60 minTypical length of a Ruby technical round

Watch: Ruby Programming Language - Full Course

Video: Ruby Programming Language - Full Course (freeCodeCamp.org, YouTube)

Test yourself and earn a certificate

10 quick questions. Score 70%+ to download your Ruby certificate.

Jump to quiz

All Questions on This Page

60 questions
Ruby Interview Questions for Freshers
  1. 1. What is Ruby and what makes it distinctive?
  2. 2. What does it mean that everything in Ruby is an object?
  3. 3. What is the difference between a symbol and a string?
  4. 4. What is nil, and what counts as false in Ruby?
  5. 5. What are the variable types in Ruby and how do you tell them apart?
  6. 6. How does string interpolation work in Ruby?
  7. 7. What are arrays and hashes in Ruby?
  8. 8. How do iterators like each work in Ruby?
  9. 9. What is a block and how do you pass one?
  10. 10. How do you define a method, and how does return work?
  11. 11. How do you define a class and its constructor?
  12. 12. What do attr_accessor, attr_reader, and attr_writer do?
  13. 13. What is the difference between puts, print, and p?
  14. 14. How do conditionals work, including unless and modifier form?
  15. 15. What are ranges and how are they used?
  16. 16. Which common string methods should you know?
  17. 17. Are Ruby operators actually methods?
  18. 18. What is the difference between require and load?
  19. 19. How do comments and naming conventions work in Ruby?
  20. 20. What does the safe navigation operator (&.) do?
Ruby Intermediate Interview Questions
  1. 21. What is the difference between blocks, procs, and lambdas?
  2. 22. How do yield and block_given? work?
  3. 23. What are modules and how do mixins work?
  4. 24. What is the difference between include, extend, and prepend?
  5. 25. What is duck typing in Ruby?
  6. 26. What does self refer to in Ruby?
  7. 27. What are public, private, and protected methods?
  8. 28. How do the spaceship operator and Comparable work together?
  9. 29. What is the Enumerable module and why does it matter?
  10. 30. What does &:method mean in something like map(&:upcase)?
  11. 31. Which hash methods and patterns should you know?
  12. 32. How does exception handling work in Ruby?
  13. 33. What does freeze do, and what is frozen_string_literal?
  14. 34. How does case/when actually match, and what is ===?
  15. 35. How do keyword arguments work, and how are they different from a hash?
  16. 36. When would you use Struct or Data?
  17. 37. What are singleton methods and the singleton class?
  18. 38. What are gems and how does Bundler manage them?
  19. 39. How do you write tests in Ruby with RSpec or Minitest?
  20. 40. How does memoization with ||= work, and where does it bite?
Ruby Interview Questions for Experienced Developers
  1. 41. How does method_missing work, and what should you pair it with?
  2. 42. How does define_method work and when is it better than method_missing?
  3. 43. How does Ruby resolve a method call?
  4. 44. How does concurrency work in Ruby, and what is the GIL?
  5. 45. Walk through Ruby's object model: classes, metaclasses, and BasicObject.
  6. 46. What does reopening a class (monkey patching) mean, and what are the risks?
  7. 47. What are refinements and when do they beat monkey patching?
  8. 48. What are instance_eval, class_eval, and instance_exec used for?
  9. 49. How does Ruby manage memory and garbage collection?
  10. 50. Can a Ruby process leak memory, and how would you diagnose it?
  11. 51. How do you reduce object allocation in a hot Ruby path?
  12. 52. How do closures and binding work in Ruby?
  13. 53. How are class methods implemented, and what does class << self mean?
  14. 54. How does Ruby on Rails structure an application, and where does Ruby's design help?
  15. 55. What is the N+1 query problem and how do you fix it in Rails?
  16. 56. How do you write thread-safe Ruby code?
  17. 57. How does Ruby's pattern matching (case/in) work?
  18. 58. What do tap and then (yield_self) do, and when are they useful?
  19. 59. What is the contract between eql? and hash for custom objects used as hash keys?
  20. 60. A Ruby service is slow or misbehaving in production. Walk through your approach.

Ruby Interview Questions for Freshers

Freshers20 questions

The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.

Q1. What is Ruby and what makes it distinctive?

Ruby is a dynamic, interpreted, object-oriented language created by Yukihiro Matsumoto with a focus on programmer happiness and readable code. It's dynamically and strongly typed and treats everything as an object.

What makes it distinctive is expressiveness: blocks, mixins via modules, open classes, and metaprogramming let you write code that reads close to plain English. Ruby on Rails is the reason most teams pick it, so its ecosystem centers on web development.

Key point: A one-line definition plus two concrete traits (everything is an object, blocks) beats a memorized feature list. Interviewers open with this to hear how you organize an answer.

Watch a deeper explanation

Video: Ruby in 100 Seconds (Fireship, YouTube)

Q2. What does it mean that everything in Ruby is an object?

Every value in Ruby, including integers, nil, true, and even classes themselves, is an object with methods. That's why you can write 5.times, "hi".upcase, or nil.to_s without special cases.

This uniformity is why Ruby has no separate primitive types like Java's int. It also explains method chaining and why numbers respond to methods you might expect to be operators.

ruby
3.times { |i| puts i }   # 0, 1, 2
-7.abs                   # 7
nil.to_a                 # []
42.class                 # Integer

Key point: Say it plainly, then give one example that surprises people (nil.to_a). That shows you have actually internalized it, not memorized the slogan.

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

A string is a mutable sequence of characters, and each string literal creates a new object. A symbol (:name) is an immutable identifier stored once in memory, so the same symbol always refers to the same object.

Use symbols for fixed names like hash keys and method references, where reuse and identity matter. Use strings for text you build, read, or change.

ruby
"hello".object_id == "hello".object_id   # false, two objects
:hello.object_id == :hello.object_id     # true, one object

user = { name: "Asha", role: "engineer" }   # symbol keys
StringSymbol
MutabilityMutableImmutable
IdentityNew object each literalSame object reused
Typical useText you build or changeHash keys, method names
Syntax"text":name

Key point: The follow-up is 'why use symbols as hash keys?'. Have the reuse and identity answer ready.

Watch a deeper explanation

Video: Ruby Basics: Symbols vs Strings (GoRails, YouTube)

Q4. What is nil, and what counts as false in Ruby?

nil is Ruby's representation of nothing, a single object of class NilClass. Methods with no explicit return often return nil.

Only false and nil are falsy. Everything else is truthy, including 0, an empty string, and an empty array. That surprises developers from C, Python, or JavaScript, where 0 or empty collections are falsy.

ruby
puts "yes" if 0            # prints yes, 0 is truthy
puts "yes" if ""           # prints yes, empty string is truthy
puts "no"  if nil          # prints nothing
name = nil
name.nil?                  # true

Key point: The trap is assuming 0 or an empty string is falsy. Naming that difference from other languages is exactly what this question screens for.

Q5. What are the variable types in Ruby and how do you tell them apart?

Ruby signals variable scope with the first character: a lowercase name or underscore is local, @name is an instance variable, @@name is a class variable, $name is global, and a capitalized name is a constant.

This naming convention is the scope rule; there's no separate declaration. Reading an uninitialized instance variable returns nil rather than raising, which is a common source of typo bugs.

ruby
count   = 1     # local
@user   = "Asha" # instance
@@total = 0     # class
$config = {}    # global
MAX_RETRIES = 3 # constant

Q6. How does string interpolation work in Ruby?

Inside double-quoted strings, #{expression} evaluates the expression and inserts its to_s result. Single-quoted strings don't interpolate; they treat #{...} literally.

Interpolation reads better than concatenation and handles any expression, not just variables. Reach for double quotes when you interpolate and single quotes for plain literals.

ruby
name = "Asha"
puts "Hello, #{name}! You have #{2 + 3} tasks."
# Hello, Asha! You have 5 tasks.
puts 'Hello, #{name}'   # Hello, #{name}  (no interpolation)

Q7. What are arrays and hashes in Ruby?

An array is an ordered, integer-indexed collection that can hold mixed types and grows dynamically. A hash is a collection of key-value pairs where keys can be any object, most often symbols.

Both come with a deep set of built-in methods. Since Ruby 1.9 hashes keep insertion order, and the symbol-key shorthand ({ name: "Asha" }) is the idiom you'll see everywhere in Rails.

ruby
nums = [3, 1, 2]
nums.sort              # [1, 2, 3]
nums << 4              # [3, 1, 2, 4]

user = { name: "Asha", age: 31 }
user[:name]            # "Asha"
user.fetch(:role, "n/a")  # "n/a", no error

Q8. How do iterators like each work in Ruby?

Iterators are methods that yield each element to a block, so you loop by passing behavior rather than managing an index. each is the base; map, select, reject, and reduce build on the same idea.

This block-based style is the idiomatic Ruby way to loop. You rarely write a for loop or a manual counter because the collection knows how to walk itself.

ruby
[1, 2, 3].each { |n| puts n }

doubled = [1, 2, 3].map { |n| n * 2 }   # [2, 4, 6]
evens   = (1..10).select { |n| n.even? } # [2, 4, 6, 8, 10]
total   = [1, 2, 3].reduce(0) { |sum, n| sum + n }  # 6

Key point: Reaching for map and select instead of each-plus-append is a strong fluency signal. Interviewers notice when someone writes Ruby like it's another language.

Q9. What is a block and how do you pass one?

A block is a chunk of code you attach to a method call, written with { } for one line or do...end for many. The method runs your block with yield, optionally passing values into the block's parameters.

Blocks are how Ruby does callbacks and iteration. They aren't objects on their own; a method receives at most one block, and you call it with yield or capture it with an &block parameter.

ruby
def repeat(n)
  n.times { |i| yield i }
end

repeat(3) { |i| puts "run #{i}" }
# run 0
# run 1
# run 2

Key point: Explaining yield out loud, and that a method takes only one block, is the bar. The follow-up is usually procs and lambdas.

Q10. How do you define a method, and how does return work?

def name(args) ... end defines a method. Parentheses on arguments are optional but conventional. The last evaluated expression is returned automatically, so an explicit return is only needed to exit early.

This implicit return is idiomatic Ruby; adding return on the final line indicates noise to experienced reviewers. Methods can take default arguments and keyword arguments too.

ruby
def greet(name, greeting: "Hello")
  "#{greeting}, #{name}"
end

greet("Asha")                 # "Hello, Asha"
greet("Ben", greeting: "Hi")  # "Hi, Ben"

Q11. How do you define a class and its constructor?

class Name ... end defines a class, and initialize is the constructor that runs when you call Name.new. Instance variables (@name) hold per-object state and are private to the object.

You expose state with accessor methods rather than direct variable access. attr_accessor, attr_reader, and attr_writer generate those methods so you don't write boilerplate getters and setters.

ruby
class User
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def greet
    "Hi, I am #{@name}"
  end
end

User.new("Asha").greet   # "Hi, I am Asha"

Q12. What do attr_accessor, attr_reader, and attr_writer do?

They generate accessor methods for instance variables so you skip writing them by hand. attr_reader makes a getter, attr_writer makes a setter, and attr_accessor makes both.

Behind the scenes attr_accessor :name defines name and name= that read and write @name. Choosing reader-only is how you make read-only attributes.

ruby
class Account
  attr_reader   :id       # read-only
  attr_accessor :balance  # read and write

  def initialize(id)
    @id = id
    @balance = 0
  end
end

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

puts writes its argument and adds a newline, calling to_s. print writes without a trailing newline. p writes the inspect form (showing quotes and structure) and returns the object, which makes it handy for debugging.

The practical rule: puts for user-facing output, p when you want to see exactly what an object is, including whether something is a string or a symbol.

ruby
puts "hi"    # hi        (returns nil)
print "hi"   # hi        (no newline)
p "hi"       # "hi"      (returns "hi")
p :hi        # :hi       (shows it is a symbol)

Q14. How do conditionals work, including unless and modifier form?

Ruby has if, elsif, else, and end, plus unless as the negation of if. Both work as modifiers at the end of a line, which reads naturally: return unless valid.

Because if is an expression, it returns a value you can assign. case/when handles multi-branch checks and uses === under the hood, so it matches ranges, classes, and regexes, not just equality.

ruby
status = if score >= 90 then "A" else "B" end

puts "denied" unless authorized

grade = case score
        when 90..100 then "A"
        when 70...90 then "B"
        else "C"
        end

Q15. What are ranges and how are they used?

A range represents an interval between two values. (1..5) includes 5 (inclusive), and (1...5) excludes it (exclusive). Ranges work with numbers, characters, and anything comparable.

They're common in iteration, case statements, and slicing. Ranges are lazy for iteration and cheap to hold, so (1..1_000_000) doesn't build a giant array.

ruby
(1..5).to_a       # [1, 2, 3, 4, 5]
(1...5).to_a      # [1, 2, 3, 4]
("a".."e").to_a   # ["a", "b", "c", "d", "e"]
(1..10).step(2).to_a  # [1, 3, 5, 7, 9]

Q16. Which common string methods should you know?

The everyday set: upcase, downcase, capitalize, strip (trim whitespace), split, gsub (global substitution), include?, and length. Ruby strings carry a deep library, so most text work is one method call.

gsub with a regex is the workhorse for find-and-replace, and split turns text into an array. Knowing these by reflex shows you write Ruby idiomatically rather than looping character by character.

ruby
"  Hello World  ".strip           # "Hello World"
"a,b,c".split(",")                # ["a", "b", "c"]
"hello".gsub("l", "L")            # "heLLo"
"Ruby".include?("ub")             # true

Q17. Are Ruby operators actually methods?

Yes. Most operators are method calls in disguise: a + b is a.+(b), and array[0] is array.[](0). That's why you can define + or [] on your own classes to make them behave like built-ins.

This is part of everything-is-an-object. Operator overloading in Ruby is just defining a method with the operator's name, so custom types can support arithmetic, comparison, and indexing.

ruby
class Vector
  def initialize(x, y) = (@x, @y = x, y)
  def +(other)
    Vector.new(@x + other.x, @y + other.y)
  end
  attr_reader :x, :y
end

Q18. What is the difference between require and load?

require loads a file once per program run and tracks what it has already loaded, so repeated requires are cheap and safe. load runs the file every time it's called and doesn't track prior loads.

You'll almost always use require (or require_relative for paths relative to the current file). load is for the rare case where you deliberately want to re-execute a file, such as reloading changed config.

Q19. How do comments and naming conventions work in Ruby?

Single-line comments #, and multi-line comments sit between =begin and =end at the start of a line comes first. Conventions carry meaning: snake_case for methods and variables, CamelCase for classes and modules, SCREAMING_SNAKE_CASE for constants.

Method names ending in ? return a boolean (empty?), and names ending in ! signal a mutating or riskier version (sort!). Teams enforce style with RuboCop rather than by memory, which is worth mentioning.

ruby
[].empty?        # true, ? means boolean
arr = [3, 1, 2]
arr.sort         # [1, 2, 3], returns a new array
arr.sort!        # sorts arr in place, ! means mutating

Key point: the ? and ! conventions is useful because matters.

Q20. What does the safe navigation operator (&.) do?

&. calls a method only if the receiver isn't nil; if it is nil, the whole expression returns nil instead of raising NoMethodError. user&.address&.city safely walks a chain that might break at any step.

It replaces long nil checks and reads cleanly. Reach for it when a value is legitimately optional; don't scatter it everywhere to paper over a value that should never be nil, because that hides real bugs.

ruby
user = nil
user&.name          # nil, no error
user.name           # NoMethodError

account&.owner&.email   # nil if any link is nil
Back to question list

Ruby Intermediate Interview Questions

Intermediate20 questions

For candidates with working experience: language mechanics, the object model, and the questions that separate users from understanders.

Q21. What is the difference between blocks, procs, and lambdas?

A block is code passed to a method, not an object on its own; a method takes one and runs it with yield. A proc and a lambda are both objects that wrap a block, so you can store them, pass several, and call them later.

Procs and lambdas differ in two ways. Lambdas check argument count and raise on a mismatch; procs ignore extras and fill missing ones with nil. A return inside a lambda exits only the lambda; a return inside a proc exits the method that created it.

ruby
square = ->(x) { x * x }   # lambda
square.call(4)             # 16

add = proc { |a, b| (a || 0) + (b || 0) }
add.call(2)                # 2, proc tolerates missing arg
# square.call(4, 5)        # ArgumentError, lambda checks arity
BlockProcLambda
Is an objectNoYesYes
Argument count checkn/aLenientStrict
return behaviorn/aExits enclosing methodExits the lambda only
Created with{ } or do..endproc { } / Proc.new-> { } / lambda { }

Key point: The arity and return differences are the whole point of this question. Have a one-line example of each ready.

Watch a deeper explanation

Video: Blocks, Procs, and Lambdas - Ruby Programming (Andrew Courter, YouTube)

Q22. How do yield and block_given? work?

yield calls the block passed to the current method, optionally handing it arguments. block_given? returns true when a block was passed, so you can make the block optional and avoid a LocalJumpError.

This pair is how you write flexible iteration methods. Capture the block explicitly with an &block parameter when you need to store it, pass it on, or convert it to a proc.

ruby
def each_pair(hash)
  return hash.to_enum(:each_pair) unless block_given?
  hash.each { |k, v| yield k, v }
end

each_pair(a: 1, b: 2) { |k, v| puts "#{k}=#{v}" }

Q23. What are modules and how do mixins work?

A module is a container for methods and constants that can't be instantiated. Its two jobs are namespacing (grouping related code) and mixins (sharing behavior across unrelated classes).

include mixes a module's instance methods into a class, extend adds them as class methods, and prepend inserts the module ahead of the class in the lookup chain. This is how Ruby shares code without multiple inheritance.

ruby
module Greetable
  def greet
    "Hi, I am #{name}"
  end
end

class User
  include Greetable
  attr_reader :name
  def initialize(name) = @name = name
end

User.new("Asha").greet   # "Hi, I am Asha"

Key point: Naming include, extend, and prepend and what each does is the bar. The classic follow-up is how modules replace multiple inheritance.

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

include adds a module's methods as instance methods of the class. extend adds them as methods on whatever object it's called on, so at class level it creates class methods. prepend inserts the module before the class itself in the ancestor chain, so its methods override the class's own.

The difference is where the module lands in method lookup. Check any class's ancestors array to see the exact order Ruby walks when resolving a method.

ruby
module Logging
  def save
    puts "logging..."
    super
  end
end

class Record
  prepend Logging
  def save = puts "saving"
end

Record.new.save   # logging...  then  saving
Record.ancestors  # [Logging, Record, ...]

Q25. What is duck typing in Ruby?

Duck typing means Ruby cares whether an object responds to the method you call, not what class it is. If it walks like a duck and quacks like a duck, it's a duck. Code works with any object that has the needed methods.

This is why explicit type checks are rare and interfaces are informal. You depend on behavior (does it respond to each?) rather than on class membership, which keeps code flexible and testable.

ruby
def print_all(collection)
  collection.each { |item| puts item }
end

print_all([1, 2, 3])          # works
print_all(1..3)               # works, Range responds to each
print_all({ a: 1 })           # works, Hash responds to each

Q26. What does self refer to in Ruby?

self is the current object, and what it points to depends on context. Inside an instance method it's the instance; inside a class body or a class method it's the class itself.

You use self to call the object's own methods explicitly, to define class methods (def self.method), and to disambiguate a setter call from creating a local variable (self.name = x, since name = x would just make a local).

ruby
class Counter
  def self.build       # class method, self is Counter
    new
  end

  attr_accessor :count
  def reset
    self.count = 0     # setter, not a local variable
  end
end

Key point: The setter gotcha (self.count = 0 versus count = 0) is a favorite follow-up. Being able to explain why the self is required shows real understanding.

Q27. What are public, private, and protected methods?

public methods can be called by anyone (the default). private methods can only be called without an explicit receiver, so they're internal to the object. protected methods can be called with an explicit receiver, but only from within the same class or its subclasses.

protected is the one people forget: it exists so an object can compare against another instance of the same class (comparing balances) while still hiding that method from outside code.

ruby
class Account
  def >(other)
    balance > other.balance   # allowed: protected + same class
  end

  protected

  def balance = @balance
end

Q28. How do the spaceship operator and Comparable work together?

The spaceship operator <=> returns -1, 0, or 1 (or nil if the two aren't comparable). Sorting and comparison methods use it under the hood.

Define <=> on your class and include Comparable, and you get <, <=, ==, >=, >, and between? for free. That's a lot of behavior from one method, and it's the idiomatic way to make custom objects sortable.

ruby
class Version
  include Comparable
  attr_reader :num
  def initialize(num) = @num = num
  def <=>(other) = num <=> other.num
end

Version.new(2) > Version.new(1)   # true
[Version.new(3), Version.new(1)].min.num  # 1

Q29. What is the Enumerable module and why does it matter?

Enumerable gives a class dozens of collection methods (map, select, reduce, sort, group_by, min, max) from a single each method. Array, Hash, Range, and Set all include it.

Define each and include Enumerable, and your own class becomes a first-class collection. This is duck typing at the library level: any object that knows how to iterate gets the full toolbox.

ruby
class Playlist
  include Enumerable
  def initialize(tracks) = @tracks = tracks
  def each(&block) = @tracks.each(&block)
end

p = Playlist.new(["a", "b", "c"])
p.map(&:upcase)   # ["A", "B", "C"]
p.select { |t| t > "a" }  # ["b", "c"]

Key point: Explaining that Enumerable is built entirely on each is the insight the question needs. It ties duck typing, mixins, and blocks together.

Q30. What does &:method mean in something like map(&:upcase)?

The & converts a symbol into a block. map(&:upcase) is shorthand for map { |x| x.upcase }: Ruby calls to_proc on the symbol, producing a proc that calls that method on each element.

It reads cleanly for one-method transforms. The same & prefix on a proc or lambda argument passes it as the method's block, so &block in a signature captures the block into a variable.

ruby
["a", "b"].map(&:upcase)         # ["A", "B"]
[1, -2, 3].select(&:positive?)   # [1, 3]

# equivalent long form
["a", "b"].map { |s| s.upcase }

Q31. Which hash methods and patterns should you know?

fetch (raises or returns a default for missing keys), dig (safe nested access), merge, each_with_object, transform_values, and group_by cover most real work. Hash.new(0) or Hash.new { |h, k| h[k] = [] } gives default values so you skip key checks.

The default-value pattern is the Ruby answer to Python's defaultdict, and it collapses counting or grouping loops to a few lines.

ruby
counts = Hash.new(0)
"mississippi".each_char { |c| counts[c] += 1 }
counts   # {"m"=>1, "i"=>4, "s"=>4, "p"=>2}

config = { db: { host: "local" } }
config.dig(:db, :host)   # "local", nil if missing

Q32. How does exception handling work in Ruby?

begin/rescue/ensure is the structure: rescue catches an exception, ensure always runs for cleanup, and else runs when nothing was raised. Inside a method or block you can skip begin and rescue directly. raise throws one; retry re-runs the begin block.

Rescue specific classes, not a bare rescue, which by default catches StandardError but is easy to misuse. Your own exceptions should subclass StandardError so they're caught by ordinary rescue clauses.

ruby
def parse(input)
  Integer(input)
rescue ArgumentError => e
  puts "bad input: #{e.message}"
  0
ensure
  puts "done"
end

Key point: Rescuing StandardError versus Exception is a common trap: rescuing Exception swallows Ctrl-C and system exits. Naming that shows care.

Q33. What does freeze do, and what is frozen_string_literal?

freeze makes an object immutable; further attempts to modify it raise a FrozenError. It's how you protect constants and shared data from accidental mutation.

The magic comment # frozen_string_literal: true at the top of a file freezes all string literals in it, which avoids allocating a new string object for repeated identical literals and prevents accidental in-place edits. Many style guides require it.

ruby
# frozen_string_literal: true

GREETING = "hello".freeze
GREETING << "!"   # FrozenError

label = "ok"
label.frozen?     # true with the magic comment

Q34. How does case/when actually match, and what is ===?

case/when compares each when value against the target using ===, the case-equality operator, not ==. Different classes define === differently: a Class checks instance membership, a Range checks inclusion, and a Regexp checks a match.

That's why case handles ranges, types, and patterns in one clean structure. Understanding === explains why case is far more flexible than a chain of == checks.

ruby
def describe(x)
  case x
  when Integer then "a number"
  when 1..10   then "small range"
  when /^a/    then "starts with a"
  else "something else"
  end
end

Integer === 5   # true, class membership check

Q35. How do keyword arguments work, and how are they different from a hash?

Keyword arguments name each parameter at the call site (def send(to:, subject:)), which makes calls self-documenting and order-independent. Required keywords raise if omitted; give a default to make one optional.

Since Ruby 3.0, keyword arguments are separated from positional hash arguments, so a trailing hash is no longer silently treated as keywords. Use ** to accept or forward arbitrary keyword pairs.

ruby
def create_user(name:, role: "member", **extra)
  { name: name, role: role, **extra }
end

create_user(name: "Asha", team: "platform")
# {name: "Asha", role: "member", team: "platform"}

Q36. When would you use Struct or Data?

Struct generates a small class with accessors, equality, and a constructor from a list of attribute names, good for lightweight mutable value objects. Data (Ruby 3.2+) does the same for immutable value objects, which is now the preferred choice when you don't need to change fields.

Both save boilerplate compared with writing a full class. Reach for Data when immutability communicates intent and Struct when you genuinely need mutability.

ruby
Point = Data.define(:x, :y)
p1 = Point.new(x: 1, y: 2)
p1.x            # 1
p1 == Point.new(x: 1, y: 2)   # true
# p1.x = 9      # NoMethodError, immutable

Q37. What are singleton methods and the singleton class?

A singleton method is defined on a single object rather than its whole class, so only that object has it. Class methods are really singleton methods on the class object, which is why def self.foo works.

Each object has a hidden singleton class (its metaclass) that holds these methods. You can open it with class << self to define several class-level methods together.

ruby
config = Object.new
def config.reload
  "reloaded"
end
config.reload   # "reloaded", only this object has it

class Service
  class << self
    def instance = @instance ||= new
  end
end

Q38. What are gems and how does Bundler manage them?

A gem is a packaged Ruby library distributed through RubyGems. You declare dependencies in a Gemfile, and Bundler resolves compatible versions and writes them to Gemfile.lock so every machine installs the exact same set.

bundle install reads the Gemfile, and bundle exec runs commands with the locked versions. The lock file is what makes builds reproducible across development, CI, and production.

ruby
# Gemfile
source "https://rubygems.org"
gem "rails", "~> 7.1"
gem "rspec", group: :test

# then: bundle install
# run with locked versions: bundle exec rspec

Q39. How do you write tests in Ruby with RSpec or Minitest?

RSpec is the popular choice: describe and context group examples, it defines a single behavior, and expect(...).to matchers assert outcomes. Minitest ships with Ruby and uses plain assert-style or spec-style syntax with less setup.

A strong answer tests behavior over implementation, uses let for lazy setup, and stubs external boundaries (network, time) rather than hitting them for real.

ruby
RSpec.describe Calculator do
  it "adds two numbers" do
    expect(Calculator.new.add(2, 3)).to eq(5)
  end

  context "with zero" do
    it "returns the other number" do
      expect(Calculator.new.add(0, 4)).to eq(4)
    end
  end
end

Q40. How does memoization with ||= work, and where does it bite?

@result ||= expensive_call computes the value on first access and caches it in an instance variable, so later calls reuse it. It expands to @result || (@result = expensive_call), so it only runs the right side when @result is nil or false.

The bite is caching a legitimately falsy result: if expensive_call returns nil or false, ||= re-runs it every time because the cache indicates unset. When false or nil is a valid answer, check defined?(@result) instead, or use a sentinel.

ruby
def config
  @config ||= load_config   # cached after first call
end

# breaks if load_config can return false or nil:
def flag
  return @flag if defined?(@flag)
  @flag = compute_flag
end

What @result ||= value evaluates step by step

1Read @result
an unset instance variable indicates nil
2Is it truthy?
if nil or false, continue; otherwise return the cached value
3Evaluate the right side
run the expensive expression once
4Assign and return
store it in @result and hand it back

This is why ||= re-runs the work whenever the cached value is nil or false. Use defined? when a falsy result is valid.

Key point: The follow-up is 'what if the value can be false?'. The defined? answer is what separates a memorized idiom from understanding it.

Back to question list

Ruby Interview Questions for Experienced Developers

Experienced20 questions

advanced rounds probe internals, metaprogramming, and production scars. Expect every answer here to draw a follow-up.

Q41. How does method_missing work, and what should you pair it with?

When you call a method that doesn't exist, Ruby walks the ancestor chain and, finding nothing, calls method_missing with the method name and arguments. Overriding it lets an object respond to methods dynamically, which is how Rails dynamic finders and proxy objects work.

Always pair it with respond_to_missing? so respond_to? and introspection stay honest, and call super for names you don't handle so real NoMethodErrors still surface. Overusing method_missing hurts readability and performance, so reach for define_method when the method set is known.

ruby
class Config
  def initialize(data) = @data = data

  def method_missing(name, *args)
    @data.key?(name) ? @data[name] : super
  end

  def respond_to_missing?(name, include_private = false)
    @data.key?(name) || super
  end
end

Key point: The follow-up is always 'why respond_to_missing?'. Answering it, and the performance cost, is what matters.

Watch a deeper explanation

Video: Ruby Tutorial | Metaprogramming in Ruby - Method Missing (Techmaker Studio, YouTube)

Q42. How does define_method work and when is it better than method_missing?

define_method creates a method at runtime from a name and a block, closing over surrounding variables. It's the right tool when you know the set of methods to generate, such as building similar accessors or wrapping an API's endpoints.

Prefer it over method_missing whenever the names are known ahead of time: define_method builds real methods that respond_to? sees, are faster to call, and show up in introspection, without the interception overhead on every unknown call.

ruby
class Report
  %i[pdf csv json].each do |format|
    define_method("to_#{format}") do
      "rendering #{format}"
    end
  end
end

Report.new.to_csv   # "rendering csv"

Q43. How does Ruby resolve a method call?

Ruby searches the object's ancestor chain in order: the singleton class, then prepended modules, the class itself, included modules (last included wins), then up through superclasses and their modules to BasicObject. The first match runs. ancestors shows the exact order.

super continues the search from the current point in that chain, which is how mixins cooperate. If nothing matches, method_missing is called, and only if that also declines does NoMethodError raise.

ruby
module A; def hi = "A"; end
module B; def hi = "B"; end
class C
  include A
  include B   # included later, wins
end

C.new.hi        # "B"
C.ancestors     # [C, B, A, Object, Kernel, BasicObject]

Q44. How does concurrency work in Ruby, and what is the GIL?

The default runtime (CRuby/MRI) has a Global VM Lock that lets only one thread run Ruby code at a time, so threads help with I/O-bound work (the lock releases during blocking I/O) but not CPU-bound parallelism. For real CPU parallelism you fork processes.

For a fuller picture: Ractors (Ruby 3+) offer actor-style parallelism with isolated object graphs, fibers give cooperative concurrency for high-I/O work, and JRuby and TruffleRuby have no GIL, so threads there run truly in parallel.

WorkloadRight tool (CRuby)Why
I/O-bound (HTTP, DB)Threads or fibersLock releases during I/O waits
CPU-bound (compute)Processes or RactorsBypass the single VM lock
High-concurrency I/OFibers (async gem)Cheap cooperative switching

Key point: The trap is saying threads are useless. Showing you know they help for I/O, and naming Ractors and non-GIL runtimes, is the senior-level answer.

Q45. Walk through Ruby's object model: classes, metaclasses, and BasicObject.

Every object has a class, and classes are themselves objects, instances of Class, which descends from Module, then Object, then BasicObject at the root. Each object also has a hidden singleton class where its per-object (singleton) methods live, including class methods on class objects.

This uniform model is why classes can have methods (they're objects), why def self.foo defines a singleton method on the class, and why you can reopen any class to add methods. It's the foundation everything else in Ruby stands on.

ruby
String.class            # Class
Class.superclass        # Module
Integer.ancestors.last  # BasicObject

5.singleton_class       # the hidden per-object class

Q46. What does reopening a class (monkey patching) mean, and what are the risks?

Ruby classes are open: you can reopen any class, including core ones like String, and add or redefine methods. Rails uses this heavily (String#blank?). Monkey patching is adding to a class you don't own.

The risks are real: name collisions between libraries, surprising behavior for anyone reading the code, and hard-to-trace bugs when two patches fight. Safer alternatives are refinements (scoped patches) and adding methods through a module you include instead.

ruby
class String
  def shout
    upcase + "!"
  end
end

"hello".shout   # "HELLO!"
# safer, scoped alternative: refinements with `using`

Q47. What are refinements and when do they beat monkey patching?

A refinement is a scoped monkey patch: you define changes inside a module with refine, then activate them with using only in a specific file or scope. Outside that scope, the core class is untouched.

They beat global patching when you want to add a method to a core class but don't want to affect the whole program or risk clashing with other libraries. The cost is lexical scoping rules that can confuse teammates, so use them deliberately.

ruby
module Shouting
  refine String do
    def shout = upcase + "!"
  end
end

using Shouting
"hi".shout   # "HI!" only where `using` is active

Q48. What are instance_eval, class_eval, and instance_exec used for?

instance_eval runs a block with self set to a given object, so you can access its internals or define singleton methods on it. class_eval (also module_eval) runs a block in a class's context, letting you define instance methods dynamically. instance_exec is like instance_eval but also passes arguments into the block.

These power DSLs: a configuration block that looks declarative is usually instance_eval changing what self means so bare method calls target your object. Use them sparingly, because they bend the normal reading of code.

ruby
class Config
  def initialize(&block)
    instance_eval(&block)   # self is this Config
  end
  def set(key, value) = (@data ||= {})[key] = value
end

Config.new { set(:env, "prod") }

Q49. How does Ruby manage memory and garbage collection?

Objects live on a managed heap, and Ruby's garbage collector reclaims unreachable ones. Modern CRuby uses a generational, incremental mark-and-sweep collector with compaction, which reduces pauses and fragmentation compared with the older stop-the-world approach.

You rarely tune it, but the model explains real issues: retained references (module-level caches, long-lived arrays) keep objects alive, and object allocation churn drives GC pressure. GC.stat and tools like derailed_benchmarks or memory_profiler help diagnose bloat.

Q50. Can a Ruby process leak memory, and how would you diagnose it?

Yes, practically. Unbounded caches, constants and class variables that only grow, global registries, and C-extension leaks all inflate a long-running process even with garbage collection, because the references keep objects reachable.

Diagnose by watching RSS over time, then use memory_profiler or ObjectSpace to see which classes are growing, and heap dumps diffed across snapshots to find the retaining path. The fix is usually a bounded cache or clearing state between requests; in Rails, tools like jemalloc and periodic worker restarts are common mitigations.

Q51. How do you reduce object allocation in a hot Ruby path?

Measure allocations first with memory_profiler or ObjectSpace on a realistic workload, because intuition is usually wrong. Common wins: freeze string literals (or the magic comment), reuse objects instead of rebuilding them in loops, prefer symbols over strings for keys, and avoid intermediate arrays by chaining lazy enumerators.

Enumerator::Lazy lets you compose map and select over a large or infinite source without materializing each intermediate array, which cuts both memory and GC pressure on big pipelines.

ruby
# eager: builds two full arrays
(1..Float::INFINITY).select(&:even?).first(3)   # hangs

# lazy: pulls only what is needed
(1..Float::INFINITY).lazy.select(&:even?).first(3)  # [2, 4, 6]

Q52. How do closures and binding work in Ruby?

Blocks, procs, and lambdas are closures: they capture the surrounding local variables by reference, so they see and can change those variables even after the enclosing method returns. This is what makes generator-style and callback code work.

A Binding object packages up that execution context (variables, self, method access), which is why binding.irb or binding.pry can drop you into a live REPL with the exact local state at that line, a genuinely useful debugging tool.

ruby
def counter
  count = 0
  increment = -> { count += 1 }
  increment.call   # 1
  increment.call   # 2
  count            # 2, the closure mutated it
end

Q53. How are class methods implemented, and what does class << self mean?

A class method is a singleton method on the class object, stored in the class's singleton class (its eigenclass). def self.build and def ClassName.build both define into that singleton class.

class << self opens the singleton class directly, so anything defined inside becomes a class-level method. It's the idiomatic way to group several class methods and to make one private with private inside that block, which the def self.foo form can't do cleanly.

ruby
class Api
  class << self
    def get(path) = request(:get, path)

    private

    def request(verb, path) = "#{verb} #{path}"
  end
end

Api.get("/users")   # "get /users"

Q54. How does Ruby on Rails structure an application, and where does Ruby's design help?

Rails is a Model-View-Controller framework built on convention over configuration: models wrap database tables through Active Record, controllers handle requests, and views render responses. Following the conventions means writing far less wiring code.

Ruby's design is what makes Rails feel declarative. Blocks and instance_eval build its DSLs (routes, migrations), method_missing and define_method power dynamic finders and attribute methods, and open classes let Active Support add methods like 2.days.ago to core types.

Key point: Connecting a Rails feature back to the Ruby mechanism behind it (dynamic finders to method_missing, routes DSL to blocks) is exactly the depth senior interviewers are checking for.

Watch a deeper explanation

Video: Learn Ruby on Rails - Full Course (freeCodeCamp.org, YouTube)

Q55. What is the N+1 query problem and how do you fix it in Rails?

An N+1 happens when you load a collection with one query, then trigger one extra query per record to load an association: 1 query for N posts, then N more for each post's author. It quietly turns a fast page into hundreds of queries.

The fix is eager loading: includes (or preload/eager_load) tells Active Record to fetch the associations up front in a couple of queries. Tools like the bullet gem flag N+1s in development before they reach production.

ruby
# N+1: one query for posts, then one per author
Post.all.each { |p| puts p.author.name }

# fixed: eager load authors in advance
Post.includes(:author).each { |p| puts p.author.name }

Key point: This is a Rails favorite that's really about understanding lazy loading. Naming the fix and a detection tool (bullet) shows production experience.

Q56. How do you write thread-safe Ruby code?

Prefer immutable data and avoid shared mutable state, because the GIL protects single bytecode operations but not multi-step read-modify-write sequences. When you must share, guard it with a Mutex, or hand work between threads through a thread-safe Queue.

Class variables and module-level state are the usual landmines in a threaded server. In Rails, the request cycle isolates most state, but memoization on shared objects and global caches need explicit synchronization or a concurrent data structure from the concurrent-ruby gem.

ruby
require "thread"

counter = 0
mutex = Mutex.new
threads = 10.times.map do
  Thread.new { 1000.times { mutex.synchronize { counter += 1 } } }
end
threads.each(&:join)
counter   # 10000, correct because of the mutex

Q57. How does Ruby's pattern matching (case/in) work?

Since Ruby 3.0, case/in destructures and matches against the shape of data: arrays, hashes, and objects, binding variables as it goes. It's deconstruction plus conditional matching in one, useful for parsing structured data like JSON or API responses.

It matches by structure and can pin existing values, add guards, and bind captures. This is a different tool from case/when: when tests with ===, while in matches and destructures shape.

ruby
config = { role: "admin", perms: ["read", "write"] }

case config
in { role: "admin", perms: [*, "write"] }
  puts "admin who can write"
in { role: String => r }
  puts "role is #{r}"
end

Q58. What do tap and then (yield_self) do, and when are they useful?

tap yields the object to a block and returns the same object, so you can run side effects (logging, mutation, debugging) in the middle of a method chain without breaking it. then (also called yield_self) yields the object and returns the block's result, so you can wrap a value in a transformation step.

tap is for peeking or configuring without interrupting a chain; then is for piping a value through a transform when a method chain would read awkwardly. Both keep code flowing left to right.

ruby
user = User.new.tap do |u|
  u.name = "Asha"
  u.role = "admin"
end   # returns the user

result = 5.then { |n| n * 2 }.then { |n| n + 1 }   # 11

Q59. What is the contract between eql? and hash for custom objects used as hash keys?

Hash and Set find keys by first comparing hash values, then confirming with eql?. So the rule is: two objects that are eql? must return the same hash, and an object's hash must not change while it's a key. Define both together or lookups silently fail.

By default eql? and hash compare object identity, so two distinct objects with the same fields won't match as keys. For a value object, override eql? to compare fields and derive hash from the same fields, usually by hashing an array of them.

ruby
class Coord
  attr_reader :x, :y
  def initialize(x, y) = (@x, @y = x, y)
  def eql?(other)
    other.is_a?(Coord) && x == other.x && y == other.y
  end
  def hash = [x, y].hash
end

seen = { Coord.new(1, 2) => "a" }
seen[Coord.new(1, 2)]   # "a", matches by value

Key point: The trap is defining == or eql? but forgetting hash. Naming that the two must agree, and why, is exactly the senior-level signal.

Q60. A Ruby service is slow or misbehaving in production. Walk through your approach.

Observe first: logs and APM traces around the slow path, then a sampling profiler like stackprof or rbspy on the live process to see whether time goes to CPU, GC, a lock, or waiting on I/O. Correlate with recent deploys and gem upgrades.

For Rails specifically, check the usual suspects: N+1 queries, missing database indexes, slow external calls without timeouts, and memory bloat forcing worker restarts. Then fix at the right layer and confirm with a measurement. The methodology, observe, hypothesize, verify, fix, confirm, is what matters.

Key point: The methodology is; tools and Rails specifics are supporting evidence.

Back to question list

Why Ruby? Ruby vs Python, PHP, and Node.js

Ruby wins when developer happiness and readable, convention-driven web work matter most, which is why Rails shops reach for it. It trades some raw execution speed for expressive syntax and one of the tightest full-stack web frameworks around. Where it loses is CPU-bound parallelism (the GIL in the default runtime) and cold-start-sensitive serverless work, where Node.js or a compiled language fits better. Knowing these trade-offs out loud is itself an interview signal: it shows you pick tools on merits, not habit.

LanguageTypingBest atWatch out for
RubyDynamicRails web apps, scripting, DSLsCPU parallelism (GIL), raw speed
PythonDynamicData, ML, scripting, automationGIL, smaller web-convention ecosystem
PHPDynamicShared-hosting web, WordPressOlder APIs, inconsistent standard library
Node.jsDynamicHigh-concurrency I/O, real-time appsCallback complexity, CPU-bound work

How to Prepare for a Ruby Interview

Prepare in layers, and practice out loud. Most Ruby rounds move from concept questions to live coding to a design or debugging discussion, often with a Rails flavor, so rehearse each stage rather than only reading answers.

  • Master your tier's concepts until you can explain them without notes, then read one tier up for the stretch questions.
  • Type and run every snippet in irb; modifying working code cements it far faster than reading.
  • Practice thinking aloud on small problems with a timer, because your process, not just the final answer is the technical point.
  • Take the quiz on this page to find weak spots, then revisit those questions before the real round.

The typical Ruby interview flow

1Recruiter or phone screen
background, motivation, a few concept checks
2Technical concepts
objects, blocks, mixins, symbols, metaprogramming
3Live coding
write and explain a method under observation
4Design or debugging
trade-offs, Rails patterns, reading unfamiliar code

Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.

Test Yourself: Ruby Quiz

Ready to test your Ruby knowledge?

10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.

10 questions Instant feedback Free certificate on 70%+

Frequently  Asked  Questions

Which Ruby topics should I prioritize before a technical round?

Prioritize the areas that change real decisions: fundamentals, practical tasks, debugging, trade-offs, and evidence. The highest-value questions are tied to a project example, a validation check, or a failure mode.

Are these questions enough to pass a Ruby interview?

They cover the question-answer portion well, but most Ruby rounds also include live coding: writing a method under observation while explaining your thinking. solving small problems out loud with a timer is the practice path. Our AI coding interview prep guide covers that format, including how automated evaluation reads your process.

Do I need to know Ruby on Rails for a Ruby interview?

It depends on the role, but most Ruby jobs are Rails jobs, so expect Rails questions unless the posting says otherwise. This page focuses on the language itself, which is what Rails is built on. Solid Ruby fundamentals, blocks, mixins, and the object model, are what make Rails answers land.

How long does it take to prepare for a Ruby interview?

If you use Ruby at work or in study, one to two weeks of an hour a day covers this bank with practice runs. Starting colder, plan three to four weeks and write code daily; reading answers without typing them is how preparation quietly fails.

Do interviewers ask exactly these questions?

The concepts repeat far more reliably than the exact wording. Interviewers rephrase, chain follow-ups, and push toward whatever you claim confidence in. Prepare the underlying ideas, blocks versus procs versus lambdas, symbols, mixins, and metaprogramming, and the phrasing takes care of itself.

Is there a way to test my Ruby knowledge?

Yes. The quiz on this page scores you as you go and explains every answer, so you find your weak spots fast. Pass the threshold and you can download a shareable certificate with your name and score, free and with no sign-up. It's the quickest way to check yourself before the real round.

From the team that builds coding interviews

Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Ruby questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.

See how the AI Coding Interviewer works

Sources

Adithyan RKWritten by Adithyan RK
Surya N
Fact-checked by Surya N
Published on: 8 May 2026Last updated: 25 Jun 2026
Share: