Skip to Content
Course content

53: The Singleton Pattern in Ruby

Click on the "Edit" button in the top corner of the screen to edit your slide content.

At some point in your project, you're going to hit a requirement where you simply cannot have more than one instance of a class. A classic example is a configuration manager. If your app loads a settings.yml file, you don't want every single service in your system reading that file from disk and creating its own copy of the settings object. That's a waste of memory, and more importantly, it creates a nightmare if you try to change a setting at runtime in one place and expect it to reflect everywhere else.

The trap of global variables and repeated instantiation

The naive way to handle this is usually to just create a global variable—the infamous $config—or to instantiate the class wherever it's needed. I've seen this in plenty of early-stage projects. You just write a Config class, and whenever you need a setting, you call Config.new.api_key.

class AppConfig
  attr_accessor :api_key

  def initialize
    # Imagine this reads from a YAML file
    @api_key = "secret_123"
  end
end

# In one part of the app
puts AppConfig.new.api_key

# In another part of the app
puts AppConfig.new.api_key

The problem here is subtle until it isn't. Right now, you're creating two entirely different objects. If you decided to update the api_key on the first instance, the second instance would still have the old value. You've essentially created a "singleton" in your head, but the code is actually creating a factory of identical-looking objects. Using a global variable ($config = AppConfig.new) solves the state problem, but globals are generally a bad smell in Ruby; they pollute the global namespace and make your code fragile and hard to test.

Taking control of the constructor

To fix this, we need to stop people from calling .new. In Ruby, .new is just a class method that allocates memory and calls initialize. We can make that method private. This forces the developer to use a specific gateway to get the instance.

class AppConfig
  @instance = new

  def self.instance
    @instance
  end

  private_class_method :new

  attr_accessor :api_key

  def initialize
    @api_key = "secret_123"
  end
end

# This now raises a NoMethodError
# AppConfig.new 

# This is the only way in
AppConfig.instance.api_key = "new_secret_456"
puts AppConfig.instance.api_key # => "new_secret_456"

This is better. We've guaranteed that there is exactly one instance of AppConfig. But there's a catch: we're manually managing the @instance variable on the class level, and we're manually hiding .new. It's boilerplate. It's "fine," but it's not how an experienced Rubyist would actually do it.

The Ruby way: The Singleton Module

Ruby provides a built-in module in the standard library specifically for this. By including Singleton, Ruby handles the private constructor and the .instance method for you automatically. It's cleaner, it's standardized, and it's thread-safe.

require 'singleton'

class AppConfig
  include Singleton

  attr_accessor :api_key

  def initialize
    @api_key = "secret_123"
  end
end

# No need to define .instance or private_class_method :new
config = AppConfig.instance
config.api_key = "final_secret_789"

puts AppConfig.instance.api_key # => "final_secret_789"

I should give you a word of caution, though. While the Singleton pattern is incredibly useful for things like connection pools or configuration, it's often criticized as an "anti-pattern" because it introduces global state. Global state makes unit testing difficult because one test might change the Singleton's value, and then the next test fails because it's expecting the default value. Whenever you use a Singleton, just remember that you're trading some architectural purity for convenience. Use it when it makes sense, but don't let every class in your app become a Singleton just because you're too lazy to pass an object as an argument.




📋 Practical Task

Build a Thread-Safe DatabaseConnectionManager

You are tasked with creating a DatabaseConnectionManager that ensures your application only ever opens one connection to the database, regardless of how many different services are requesting it. If you open multiple connections, the database will crash under the load.

Requirements:

  • Use the Singleton module from the Ruby standard library.
  • The class should have an initialize method that sets a @connection_id to a random number (to simulate a unique connection handle).
  • Create a method called execute_query(sql) that prints: "Executing [sql] using connection [connection_id]".
  • In your implementation script, attempt to call DatabaseConnectionManager.new and verify that it raises a NoMethodError.
  • Instantiate the manager twice using .instance and prove that both variables point to the exact same object (use the equal? method or check the object_id).
  • Call execute_query from both references to confirm they are using the same connection ID.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.