David Morales David Morales
/
Single ruby gemstone connected by threads to multiple empty sockets.

Some objects shouldn’t exist more than once. A logger writing to a single file, an application-wide settings registry, a database connection pool: these are resources where having two instances isn’t just redundant but potentially dangerous. Two loggers writing simultaneously can corrupt the output. Two configuration objects can silently diverge, and your application will behave differently depending on which one it consults.

The Singleton pattern addresses this problem head-on: it guarantees that a class produces exactly one instance and provides a global point of access to it.

In this article we’ll implement a Singleton step by step, explore both Ruby’s built-in support and the manual approach, and then tackle the pattern’s controversial side: why it’s often warned against, and when it’s the right choice.

The Problem: When You Need Exactly One

Say we’re building a small Ruby application that reads its settings from a YAML file. Several parts of the system need access to the same configuration: the database layer needs the connection string, the mailer module needs the SMTP host, and the logging module needs the log level. We start with a simple class:

require 'yaml'
class AppConfig
attr_reader :settings
def initialize(path = 'config.yml')
@settings = YAML.load_file(path)
end
def get(key)
@settings[key]
end
end

Any part of the application can create an instance and read the settings:

config = AppConfig.new
puts config.get('database_host') # => "localhost"
puts config.get('log_level') # => "info"

This works. But a problem shows up quickly: every time someone writes AppConfig.new, the file is read from disk again, a new object is allocated, and a separate copy of the settings lives in memory. If the application has fifty classes that need configuration, that’s fifty file reads and fifty copies.

Worse: imagine one part of the system modifies its copy of the settings at runtime (say, switching the log level to debug for troubleshooting). That change is invisible to every other copy. The application is now running with inconsistent state, and the bug it produces will be subtle and hard to track down.

What we need is a way to guarantee that AppConfig produces exactly one instance, and that everyone who asks for it gets the same object.

Ruby’s Built-In Singleton Module

Ruby provides a Singleton module in its standard library. Including it in a class gives you an instance method and makes new private, so no additional instances can be created:

require 'singleton'
require 'yaml'
class AppConfig
include Singleton
attr_reader :settings
def initialize
@settings = YAML.load_file('config.yml')
end
def get(key)
@settings[key]
end
end

Now instead of calling AppConfig.new, you call AppConfig.instance:

config_a = AppConfig.instance
config_b = AppConfig.instance
puts config_a.object_id == config_b.object_id # => true

Both variables point to the same object. The file is read once, the settings live in a single place, and any runtime modification is visible everywhere. If you try calling AppConfig.new directly, Ruby raises a NoMethodError because new is now private.

The Singleton module also handles thread safety internally, using a mutex to ensure that even if two threads call instance at the exact same moment, only one instance gets created.

Manual Implementation

The built-in module is convenient, but understanding how to build a Singleton manually reveals what’s happening under the surface. Here’s the idiomatic Ruby approach:

require 'yaml'
class AppConfig
def self.instance
@instance ||= new
end
def get(key)
@settings[key]
end
private
attr_reader :settings
def initialize
@settings = YAML.load_file('config.yml')
end
private_class_method :new
end

There are three key points in this implementation:

The Class Variable Trap

You might be tempted to use a class variable (@@instance) instead:

class AppConfig
def self.instance
@@instance ||= new
end
private_class_method :new
# ...
end

This works in isolation, but class variables in Ruby are shared across the entire inheritance hierarchy. If you create a subclass, both the parent and child will share the same @@instance:

class AppConfig
def self.instance
@@instance ||= new
end
def who_am_i
self.class.name
end
private_class_method :new
end
class TestConfig < AppConfig
private_class_method :new
end
first = AppConfig.instance
second = TestConfig.instance
puts first.who_am_i # => "AppConfig"
puts second.who_am_i # => "AppConfig" (not "TestConfig"!)

TestConfig.instance returns the AppConfig instance because @@instance was already set by the parent class. The subclass never gets its own singleton.

The class instance variable approach (@instance) doesn’t have this problem, because each class in the hierarchy has its own @instance:

class AppConfig
def self.instance
@instance ||= new
end
def who_am_i
self.class.name
end
private_class_method :new
end
class TestConfig < AppConfig
private_class_method :new
end
first = AppConfig.instance
second = TestConfig.instance
puts first.who_am_i # => "AppConfig"
puts second.who_am_i # => "TestConfig"

Now each class maintains its own singleton instance.

Thread Safety

The manual implementation has a subtle vulnerability. Watch what happens if two threads call self.instance simultaneously, before any instance exists:

Two instances were created. Thread A already holds a reference to the first one, but Thread B overwrote @instance with the second. From that point on, any new call to self.instance returns the second instance, while Thread A keeps using the first. There are two objects where there should be one, which can lead to data corruption or inconsistent behavior.

Ruby’s built-in Singleton module avoids this with a mutex. If you’re implementing the pattern manually and your application is multithreaded, you’ll need to add one:

require 'yaml'
class AppConfig
MUTEX = Mutex.new
def self.instance
MUTEX.synchronize { @instance ||= new }
end
def get(key)
settings[key]
end
private
def initialize
@settings = YAML.load_file('config.yml')
end
def settings
@settings
end
private_class_method :new
end

The Mutex#synchronize block ensures that only one thread at a time can execute the creation logic. The cost is minimal (one lock acquisition per call), and once the instance exists it’s returned directly.

We can verify that both threads get the same object:

require 'singleton'
class AppConfig
include Singleton
def get(key)
{ 'log_level' => 'info' }[key]
end
end
ids = []
t1 = Thread.new { ids << AppConfig.instance.object_id }
t2 = Thread.new { ids << AppConfig.instance.object_id }
t1.join
t2.join
puts ids[0] == ids[1] # => true

Each thread calls AppConfig.instance independently, but both receive the same object. The matching object_id confirms it.

Why Singletons Are Controversial

The Singleton pattern is probably the most criticized of the 23 GoF patterns, because of what it encourages: global mutable state.

Let’s see why. Here’s a service that uses our AppConfig singleton:

class UserMailer
def send_welcome(user)
host = AppConfig.instance.get('smtp_host')
port = AppConfig.instance.get('smtp_port')
# ... send the email using host and port
end
end

It looks clean. UserMailer reaches into AppConfig.instance whenever it needs a setting. But there are three problems hiding behind that simplicity.

  1. Hidden dependencies. Looking at UserMailer’s interface (its class name, its public methods, its constructor), nothing reveals a dependency on AppConfig. The dependency is buried inside the method body. A developer reading the code has to inspect every method to figure out what UserMailer actually needs to work. In a small application this is manageable. In a large one with hundreds of classes, hidden dependencies make the system opaque.

  2. Tight coupling. This problem is closely related to the previous one: every class that calls AppConfig.instance doesn’t just hide the dependency, it’s permanently tied to that specific class. If you later need a different configuration source (environment variables instead of YAML, or a remote config service), you have to find and change every call site.

  3. Testing friction. As a consequence of the two points above, to test the send_welcome method you need to control the host and port values. But AppConfig.instance returns the real singleton, loaded from the real config file. To test with different values, you either have to manipulate the singleton’s internal state (fragile) or monkey-patch the instance method to return a controlled object (ugly). Neither option is pleasant:

# Option 1: Modify internal state (fragile, couples tests to implementation)
AppConfig.instance.instance_variable_set(:@settings, {
'smtp_host' => 'test.example.com',
'smtp_port' => 2525
})
# Option 2: Monkey-patch instance to return a test double
class FakeConfig
def get(key)
{ 'smtp_host' => 'test.example.com', 'smtp_port' => 2525 }[key]
end
end
class AppConfig
def self.instance
FakeConfig.new
end
end

Both approaches are workarounds for a design problem. Having to fight the pattern to test your code is a signal.

Using Dependency Injection

The solution to all three problems is dependency injection: instead of letting each class use the singleton on its own, you hand it the dependency it needs from the outside.

class UserMailer
def initialize(config)
@config = config
end
def send_welcome(user)
host = @config.get('smtp_host')
port = @config.get('smtp_port')
# ... send the email using host and port
end
end

Now UserMailer declares its dependency explicitly in the constructor. Testing becomes trivial:

fake_config = { 'smtp_host' => 'test.example.com', 'smtp_port' => 2525 }
class FakeConfig
def initialize(data)
@data = data
end
def get(key)
@data[key]
end
end
mailer = UserMailer.new(FakeConfig.new(fake_config))
mailer.send_welcome(user)

This way you pass in what you need, and the test controls the environment completely.

What’s more, the class no longer cares whether the config object is a Singleton, a plain object, or a mock. It just needs something that responds to get. This is the power of depending on messages rather than specific classes.

Module Methods: Ruby’s Natural Singleton

Ruby has a language construct that behaves like a Singleton: modules with module_function. Since modules can’t be instantiated, there’s inherently only one “instance” of their behavior:

require 'yaml'
module AppConfig
@settings = YAML.load_file('config.yml')
module_function
def get(key)
@settings[key]
end
end
# Usage
AppConfig.get('database_host') # => "localhost"
AppConfig.get('log_level') # => "info"

This is simpler than a Singleton class:

You just call the methods directly on the module.

However, modules have a limitation: they have no constructor, so their state is initialized the moment Ruby loads the .rb file that contains the module (when require runs). All code in the module body executes immediately, including @settings = YAML.load_file('config.yml'). If the config.yml file doesn’t exist yet at that point, the application will crash on startup. With a Singleton class, the file read is deferred until the first call to instance. If you need that lazy initialization, a class-based approach is more appropriate.

For stateless or read-only services (configuration, log formatters, utility functions), a module is often the most idiomatic Ruby solution.

When Singleton Is the Right Choice

Given all the warnings, when should you actually use the Singleton pattern?

The pattern is a good fit when all of these conditions are true:

Summary: Choosing the Right Approach

Here’s a practical guide for deciding how to implement a Singleton in Ruby:

Use Ruby’s native Singleton module when you need a straightforward singleton with thread safety, lazy initialization, and the guarantee that only one instance exists. It’s the canonical implementation.

Use a manual implementation when you want finer control over initialization, need to support subclassing, or want to understand exactly what’s going on. Remember to use @instance (class instance variable), not @@instance (class variable), and add a mutex if your application is multithreaded.

Use a module with module_function when the singleton behavior is stateless or read-only. It’s the most idiomatic Ruby approach for configuration readers and formatters.

Use dependency injection when you need testability and flexibility. You can still create the object once at the application’s composition root and pass it to everything that needs it. You get the benefits of a single instance without the downsides of global state.

Conclusion

The Singleton pattern ensures that a class has exactly one instance. Ruby makes it easy to implement, both through the standard library’s native Singleton module and through manual techniques that leverage private_class_method and memoization.

But the pattern has a cost when used directly. Every call to SomeClass.instance scattered across the codebase is a hidden dependency, a coupling point, and a testing obstacle. Creating the instance once and passing it through dependency injection preserves the single-instance guarantee while keeping the code testable and flexible.

Use Singleton when the problem genuinely calls for it: objects that manage external resources where duplication would cause real harm. For everything else, prefer explicit dependencies.

Test your knowledge

  1. What does private_class_method :new do in a manual Singleton implementation?

  1. Why should you prefer @instance over @@instance in a manual Singleton?

  1. What is the purpose of Mutex#synchronize in the manual Singleton implementation?

  1. What is the main advantage of using dependency injection with a Singleton?

  1. What limitation do modules with module_function have compared to a Singleton class?