The Null Object Pattern in Ruby

· 7 min read · Updated April 1, 2026 · intermediate
ruby null-object design-patterns nil guides

What Is the Null Object Pattern?

The Null Object pattern replaces nil references with objects that implement the same interface as real objects but do nothing. Instead of checking if user.nil? everywhere, you inject a null version of User that responds to the same methods but returns safe, harmless values.

This moves behaviour from absence of data into presence of a well-defined nothing. The calling code never needs to branch on nil — it just calls methods.

The payoff is real:

  • Conditionals disappear from calling code
  • New edge cases cannot sneak in (you never forget a nil check)
  • The “nothing” behaviour lives in one place, not scattered across every consumer

A Simple Example

Imagine a Greeter class that formats greetings:

class Greeter
  def initialize(name)
    @name = name
  end

  def greet
    "Hello, #{@name}!"
  end
end

Now imagine some callers are passed nil for the name. You might write:

def display_greeting(greeter)
  return "No greeter provided" if greeter.nil?
  greeter.greet
end

The Null Object pattern solves this differently. You create a NullGreeter that satisfies the same interface:

class Greeter
  def initialize(name)
    @name = name
  end

  def greet
    "Hello, #{@name}!"
  end
end

class NullGreeter
  def greet
    ""
  end
end

Now your factory decides which one to return:

def build_greeter(name)
  name ? Greeter.new(name) : NullGreeter.new
end

Calling code no longer checks for nil:

greeter = build_greeter(user_name)
puts greeter.greet  # always safe — no nil check needed

Implementing Null Objects in Ruby

The key contract is respond to the same messages. The null version should return values that are truthy or harmless, never ones that cause further errors downstream.

Struct-Based Null Objects

Ruby Struct makes null objects almost free. Define the interface once:

class User
  attr_reader :name, :email

  def initialize(name:, email:)
    @name = name
    @email = email
  end

  def full_identifiers
    "#{name} <#{email}>"
  end
end

NullUser = Struct.new(:name, :email) do
  def full_identifiers
    ""
  end
end

def find_user(id)
  # ...database lookup...
  id == 0 ? NullUser.new("Anonymous", "noreply@example.com") : User.new(name: "Alice", email: "alice@example.com")
end

Every caller that works with User also works with NullUser — the struct shares the same attr_readers.

Module-Based Shared Interface

When the interface has many methods, share it with a module:

module Billable
  def base_rate
    raise NotImplementedError
  end

  def discount_eligible?
    raise NotImplementedError
  end

  def billable_hours
    raise NotImplementedError
  end
end

class StandardPlan
  include Billable

  def base_rate
    99.0
  end

  def discount_eligible?
    false
  end

  def billable_hours
    0
  end
end

class NullPlan
  include Billable

  def base_rate
    0.0
  end

  def discount_eligible?
    false
  end

  def billable_hours
    0
  end
end

Both classes satisfy the same module interface. Callers can invoke any Billable method without checking for nil.

Null Collections

A common source of nil checks is code like:

users = fetch_users(user_id)
return if users.nil?
users.each { |u| puts u.name }

Instead, always return an empty collection from your data layer:

def fetch_users(user_id)
  results = DB.query("SELECT * FROM users WHERE group_id = ?", user_id)
  results.map { |row| User.new(row) }
  # Always returns an Array, even if DB returns nothing
end

If you need a richer null collection that still quacks like a real one:

class NullCollection
  def each
    return enum_for(:each) unless block_given?
    # no-op: empty iteration
  end

  def to_a
    []
  end

  def first
    nil
  end

  def empty?
    true
  end

  def respond_to?(method, *)
    method == :each || super
  end
end

Callers can iterate, call empty?, or call first without any nil guards.

Ruby’s Safe Navigation Operator as an Alternative

Ruby 2.3 introduced the safe navigation operator &. which short-circuits method calls on nil:

user = User.find_by(email: "nobody@example.com")
# => nil

user&.name
# => nil

user&.profile&.avatar&.url
# => nil — no NoMethodError, no stack trace

&. is useful when you genuinely do not know whether a value exists and you want a short expression to fall through to nil. It shines for deep attribute chains.

However, &. only solves this method call. It does not eliminate the problem — you still need to handle nil at some point. If you find yourself chaining six &. operators or wrapping everything in || nil, the Null Object pattern is the better answer.

Use &. for shallow, one-off nil checks. Use Null Objects when nil is a first-class business case with its own set of behaviours.

The nil?.present? Pattern

A common Ruby idiom combines nil? and present? (from Active Support):

name = params[:name]
return "Unknown" if name.nil? || name.blank?

name.upcase

present? returns false for nil, false, empty strings, and empty collections. It is useful for gating logic, but it is still a conditional. If you need this gate in many places, a Null Object with sensible defaults is cleaner:

class NullName
  def upcase
    "UNKNOWN"
  end

  def blank?
    true
  end
end

Now the caller simply calls name.upcase and the NullName handles the “unknown” case in one place.

ActiveSupport::NilPlaceholder

Rails’ ActiveSupport::NilPlaceholder is a utility class specifically designed for cases where you need to represent “no value” but calling methods on it should return the placeholder itself, enabling chaining:

require "active_support/all"

result = ActiveSupport::NilPlaceholder.new

result.foo
# => nil

result.foo.bar
# => nil  — no NoMethodError

This is useful when passing objects into views or templates where calling undefined methods should silently return nil rather than raising exceptions. However, for domain logic where you want specific “nothing” behaviour (empty string, zero, empty array), a custom Null Object with defined return values is more expressive.

Practical Examples

Null Logger

Logging is everywhere. Passing a real logger in tests or CLI scripts is noise. A Null Logger solves this:

class NullLogger
  def debug(*); end
  def info(*); end
  def warn(*); end
  def error(*); end
  def unknown(*); end
  def add(*); end
  def <<(msg); end
end

class ApplicationService
  def initialize(logger: NullLogger.new)
    @logger = logger
  end

  def run
    @logger.info "Starting process"
    # ... work ...
    @logger.info "Finished"
  end
end

In production you pass Logger.new(STDOUT). In tests you pass NullLogger.new. The service never branches on which logger it received.

Null Mailer

Email dispatchers often need to be disabled in testing or development:

class NullMailer
  def deliver(_message)
    # no-op
  end

  def deliver_now(_message)
    # no-op
  end

  def deliver_later(_message)
    # no-op — fire and forget
  end
end

class OrderConfirmation
  def initialize(mailer: NullMailer.new)
    @mailer = mailer
  end

  def send_confirmation(order)
    @mailer.deliver_now(build_email(order))
  end
end

Null Presenter

Presenters wrap domain objects for view rendering. When a record is missing, a Null Presenter supplies safe defaults:

class UserPresenter
  def initialize(user)
    @user = user
  end

  def display_name
    @user.name
  end

  def avatar_url
    @user.avatar.url
  end

  def member_since
    @user.created_at.strftime("%B %Y")
  end
end

class NullPresenter
  def display_name
    "Guest"
  end

  def avatar_url
    "/images/default-avatar.svg"
  end

  def member_since
    "—"
  end
end

def present_user(user)
  user ? UserPresenter.new(user) : NullPresenter.new
end

Views can call present_user(order.user).display_name without any || "Guest" inline.

When to Use Null Objects vs Raising Exceptions

Null Objects and exceptions solve different failure modes. Use this as a guide:

SituationApproach
”I cannot find this record” is a normal, expected outcomeNull Object
The data being missing is an error conditionRaise nil → raise an exception
”Nothing here” is a valid business stateNull Object
Calling code asked for something that does not existRaise nil → raise an exception
You always expect a valid object and absence is a bugRaise an exception
Absence is a first-class case with defined behaviourNull Object

The Null Object pattern is not a replacement for proper error handling. If find_user failing to find a user is genuinely exceptional, raise an exception. If “no user found” is a valid state that calling code should handle gracefully, a Null Object is the right tool.

The key question to ask: is the absence of this object a valid business state with its own behaviour? If yes, reach for Null Objects. If no, raise an exception.

See Also