The Null Object Pattern in Ruby
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
nilcheck) - 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:
| Situation | Approach |
|---|---|
| ”I cannot find this record” is a normal, expected outcome | Null Object |
| The data being missing is an error condition | Raise nil → raise an exception |
| ”Nothing here” is a valid business state | Null Object |
| Calling code asked for something that does not exist | Raise nil → raise an exception |
| You always expect a valid object and absence is a bug | Raise an exception |
| Absence is a first-class case with defined behaviour | Null 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
- /guides/ruby-service-objects/ — Service objects complement Null Objects well; both move branching logic out of callers
- /guides/ruby-value-objects/ — Value Objects and Null Objects both encapsulate behaviour around data representations
- /guides/ruby-struct-guide/ — Struct is often the fastest path to a Null Object implementation in Ruby