Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
53: The Singleton Pattern in Ruby
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
Singletonmodule from the Ruby standard library. - The class should have an
initializemethod that sets a@connection_idto 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.newand verify that it raises aNoMethodError. - Instantiate the manager twice using
.instanceand prove that both variables point to the exact same object (use theequal?method or check theobject_id). - Call
execute_queryfrom both references to confirm they are using the same connection ID.
There are no comments for now.