Singleton Pattern
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] endendAny 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] endendNow instead of calling AppConfig.new, you call AppConfig.instance:
config_a = AppConfig.instanceconfig_b = AppConfig.instance
puts config_a.object_id == config_b.object_id # => trueBoth 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 :newendThere are three key points in this implementation:
-
private_class_method :newprevents anyone outside the class from callingAppConfig.new. Ifnewwere public, the Singleton could be bypassed and a second instance created. -
self.instanceis the only way to obtain the object. It’s a class method, so you call it on the class itself, not on an instance. -
@instance ||= newis Ruby’s memoization idiom. The first timeself.instanceis called,@instanceisnil, sonewruns and the result is stored. Every subsequent call returns the stored object without creating a new one.Note that
@instancehere is a class instance variable, not a regular instance variable. This means class instance variables belong to the class object itself, not to instances of the class. Since there’s only one class object, there’s only one@instance.
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
# ...endThis 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 :newend
class TestConfig < AppConfig private_class_method :newend
first = AppConfig.instancesecond = 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 :newend
class TestConfig < AppConfig private_class_method :newend
first = AppConfig.instancesecond = 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:
- Thread A: reads
@instance→nil - Thread B: reads
@instance→nil - Thread A: creates new instance, assigns to
@instance - Thread B: creates another new instance, overwrites
@instance
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 :newendThe 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] endend
ids = []
t1 = Thread.new { ids << AppConfig.instance.object_id }t2 = Thread.new { ids << AppConfig.instance.object_id }
t1.joint2.join
puts ids[0] == ids[1] # => trueEach 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 endendIt looks clean. UserMailer reaches into AppConfig.instance whenever it needs a setting. But there are three problems hiding behind that simplicity.
-
Hidden dependencies. Looking at
UserMailer’s interface (its class name, its public methods, its constructor), nothing reveals a dependency onAppConfig. The dependency is buried inside the method body. A developer reading the code has to inspect every method to figure out whatUserMaileractually needs to work. In a small application this is manageable. In a large one with hundreds of classes, hidden dependencies make the system opaque. -
Tight coupling. This problem is closely related to the previous one: every class that calls
AppConfig.instancedoesn’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. -
Testing friction. As a consequence of the two points above, to test the
send_welcomemethod you need to control the host and port values. ButAppConfig.instancereturns 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 theinstancemethod 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 doubleclass FakeConfig def get(key) { 'smtp_host' => 'test.example.com', 'smtp_port' => 2525 }[key] endend
class AppConfig def self.instance FakeConfig.new endendBoth 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 endendNow 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] endend
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] endend
# UsageAppConfig.get('database_host') # => "localhost"AppConfig.get('log_level') # => "info"This is simpler than a Singleton class:
-
No
instancemethod to call. -
No
newto make private. -
No mutex to manage.
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:
-
There must be exactly one instance for the program to work correctly.
-
The instance needs to be accessible from multiple unrelated parts of the system.
-
The object manages a resource that’s expensive to create or dangerous to duplicate.
Examples:
-
A logger writing to a single file (two loggers writing simultaneously can interleave lines and corrupt the output).
-
A database connection pool (if each module maintained its own pool, the database would be overwhelmed with connections).
-
An application-wide configuration registry (divergent copies would lead to inconsistent behavior, as we saw at the beginning of this article).
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
-
What does
private_class_method :newdo in a manual Singleton implementation?
-
Why should you prefer
@instanceover@@instancein a manual Singleton?
-
What is the purpose of
Mutex#synchronizein the manual Singleton implementation?
-
What is the main advantage of using dependency injection with a Singleton?
-
What limitation do modules with
module_functionhave compared to a Singleton class?