CrackedRuby CrackedRuby

Overview

The Singleton pattern ensures that a class has only one instance and provides a global point of access to that instance. This pattern addresses scenarios where exactly one object coordinates actions across a system, such as managing shared resources, coordinating system-wide operations, or maintaining global state.

The pattern originated from the Gang of Four's Design Patterns book (1994) as one of the creational patterns. The core mechanism prevents multiple instantiations by controlling object creation through the class itself rather than allowing clients to create instances directly.

# Basic singleton structure
class Database
  @instance = nil
  
  def self.instance
    @instance ||= new
  end
  
  private_class_method :new
end

db1 = Database.instance
db2 = Database.instance
db1.object_id == db2.object_id  # => true

The pattern solves the problem of uncontrolled object proliferation for resources that should remain unique. Without singleton enforcement, nothing prevents multiple database connection managers, configuration objects, or thread pools from existing simultaneously, leading to resource conflicts and inconsistent state.

Applications frequently require singleton instances for logging systems, where all parts of an application write to a single log manager; caching mechanisms, where a unified cache prevents redundant storage; and hardware interface controllers, where multiple instances could cause device conflicts.

Key Principles

The Singleton pattern operates on three fundamental principles: restricted instantiation, lazy initialization, and global access. These principles work together to create a controlled instance lifecycle.

Restricted Instantiation prevents external code from creating new instances. The class constructor becomes private or protected, making direct instantiation impossible. The class itself assumes responsibility for creating its single instance, maintaining complete control over the instantiation process.

Lazy Initialization defers instance creation until first access. The instance comes into existence only when code first requests it, avoiding unnecessary resource allocation for unused singletons. This approach reduces application startup time and memory footprint when singletons remain unreferenced during certain execution paths.

class Cache
  @instance = nil
  
  def self.instance
    @instance ||= new  # Created only on first access
  end
  
  private_class_method :new
  
  def initialize
    @data = {}
    puts "Cache initialized"
  end
end

# Nothing printed yet - no instance created
cache1 = Cache.instance  # Prints "Cache initialized"
cache2 = Cache.instance  # Prints nothing - reuses instance

Global Access provides a consistent mechanism for retrieving the singleton instance. The class exposes a static method (typically named instance or get_instance) that returns the sole instance. This access point remains available throughout the application's scope, eliminating the need to pass the instance through constructor chains or dependency graphs.

The singleton lifecycle follows a specific sequence: class loading, instance creation on first access, instance reuse on subsequent access, and instance cleanup at application termination. The class maintains instance state across all access points, creating a shared resource that persists for the application's duration.

Thread safety represents a critical principle in multi-threaded environments. Without proper synchronization, concurrent threads might create multiple instances during simultaneous first access, violating the singleton guarantee. Thread-safe implementations require careful consideration of race conditions during instance creation.

Ruby Implementation

Ruby provides multiple mechanisms for implementing singletons, each with different characteristics and use cases. The language includes a built-in Singleton module that handles the implementation details automatically.

Standard Library Singleton Module

Ruby's standard library provides a Singleton module that implements the pattern correctly with thread safety:

require 'singleton'

class Logger
  include Singleton
  
  def initialize
    @log_file = File.open('app.log', 'a')
  end
  
  def write(message)
    @log_file.puts("[#{Time.now}] #{message}")
    @log_file.flush
  end
end

log1 = Logger.instance
log2 = Logger.instance
log1.equal?(log2)  # => true

# Attempting to create new instance raises error
begin
  Logger.new
rescue NoMethodError => e
  puts e.message  # => "private method `new' called for Logger:Class"
end

Including the Singleton module automatically makes the new method private and adds a thread-safe instance class method. The module handles all synchronization concerns, making it the preferred approach for most singleton implementations.

Manual Implementation with Class Instance Variables

Manual singleton implementation uses class instance variables to store the single instance:

class Configuration
  @instance = nil
  @mutex = Mutex.new
  
  def self.instance
    return @instance if @instance
    
    @mutex.synchronize do
      @instance ||= new
    end
  end
  
  private_class_method :new
  
  attr_reader :settings
  
  def initialize
    @settings = load_settings
  end
  
  private
  
  def load_settings
    { database: 'postgresql', cache: 'redis' }
  end
end

config = Configuration.instance
config.settings  # => {:database=>"postgresql", :cache=>"redis"}

This approach provides explicit control over instance creation and synchronization. The double-checked locking pattern (checking @instance before and after acquiring the mutex) minimizes lock contention after initialization completes.

Module-Based Singleton

Ruby modules themselves act as singletons since each module exists as a single object:

module AppState
  @data = {}
  
  def self.set(key, value)
    @data[key] = value
  end
  
  def self.get(key)
    @data[key]
  end
  
  def self.clear
    @data.clear
  end
end

AppState.set(:user_id, 42)
AppState.get(:user_id)  # => 42

Modules provide natural singleton behavior without requiring explicit singleton implementation. This approach works well for stateless utility collections or simple state managers.

Eager Initialization

Some scenarios require creating the singleton instance immediately at class load time rather than on first access:

class DatabasePool
  INSTANCE = new
  
  def self.instance
    INSTANCE
  end
  
  private_class_method :new
  
  attr_reader :connections
  
  def initialize
    @connections = Array.new(5) { create_connection }
  end
  
  private
  
  def create_connection
    # Simulated connection creation
    Object.new
  end
end

# Instance created when class loads
pool = DatabasePool.instance
pool.connections.size  # => 5

Eager initialization eliminates lazy loading benefits but guarantees instance availability and removes thread safety concerns during creation.

Singleton with Inheritance

Ruby's singleton module supports inheritance, though each subclass creates its own singleton instance:

require 'singleton'

class DataStore
  include Singleton
  
  attr_accessor :data
  
  def initialize
    @data = {}
  end
end

class UserStore < DataStore
  def initialize
    super
    @data[:type] = 'users'
  end
end

store1 = DataStore.instance
store2 = UserStore.instance

store1.equal?(store2)  # => false (different singleton instances)
store1.class  # => DataStore
store2.class  # => UserStore

Each class in the hierarchy maintains its own singleton instance, creating independent singleton families rather than a shared singleton across inheritance chains.

Practical Examples

Configuration Manager

Applications commonly use singletons to manage configuration data loaded from files or environment variables:

require 'singleton'
require 'yaml'

class AppConfig
  include Singleton
  
  attr_reader :settings
  
  def initialize
    @settings = load_config
  end
  
  def database_url
    @settings['database']['url']
  end
  
  def cache_enabled?
    @settings['cache']['enabled']
  end
  
  def feature_flag(name)
    @settings.dig('features', name.to_s) || false
  end
  
  private
  
  def load_config
    if File.exist?('config.yml')
      YAML.load_file('config.yml')
    else
      default_config
    end
  end
  
  def default_config
    {
      'database' => { 'url' => 'postgresql://localhost/app' },
      'cache' => { 'enabled' => true },
      'features' => { 'new_ui' => false }
    }
  end
end

# Usage throughout application
config = AppConfig.instance
db_url = config.database_url
cache_on = config.cache_enabled?

This pattern centralizes configuration access and ensures consistent settings across all application components. The singleton loads configuration once and provides it on demand without file system overhead.

Connection Pool Manager

Database connection pools benefit from singleton implementation to prevent multiple pools competing for the same connections:

require 'singleton'

class ConnectionPool
  include Singleton
  
  def initialize
    @pool = []
    @mutex = Mutex.new
    @max_connections = 10
    @available = []
    @in_use = []
  end
  
  def acquire
    @mutex.synchronize do
      connection = @available.pop
      
      if connection.nil? && @in_use.size < @max_connections
        connection = create_connection
      end
      
      return nil if connection.nil?
      
      @in_use << connection
      connection
    end
  end
  
  def release(connection)
    @mutex.synchronize do
      @in_use.delete(connection)
      @available << connection
    end
  end
  
  def stats
    @mutex.synchronize do
      {
        available: @available.size,
        in_use: @in_use.size,
        total: @available.size + @in_use.size
      }
    end
  end
  
  private
  
  def create_connection
    # Simulated connection creation
    { id: rand(1000), created_at: Time.now }
  end
end

# Application usage
pool = ConnectionPool.instance
conn1 = pool.acquire
conn2 = pool.acquire

pool.stats  # => {:available=>0, :in_use=>2, :total=>2}

pool.release(conn1)
pool.stats  # => {:available=>1, :in_use=>1, :total=>2}

The singleton pool coordinates connection distribution across the entire application, preventing connection exhaustion and managing resources efficiently.

Application-Wide Logger

Logging systems use singletons to consolidate all logging activity into a single output stream:

require 'singleton'

class ApplicationLogger
  include Singleton
  
  LEVELS = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }
  
  attr_accessor :level
  
  def initialize
    @level = :info
    @output = $stdout
    @mutex = Mutex.new
  end
  
  def debug(message)
    log(:debug, message)
  end
  
  def info(message)
    log(:info, message)
  end
  
  def warn(message)
    log(:warn, message)
  end
  
  def error(message)
    log(:error, message)
  end
  
  def with_context(context_data)
    Thread.current[:log_context] = context_data
    yield
  ensure
    Thread.current[:log_context] = nil
  end
  
  private
  
  def log(level, message)
    return if LEVELS[level] < LEVELS[@level]
    
    @mutex.synchronize do
      timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
      context = Thread.current[:log_context]
      context_str = context ? " [#{context}]" : ""
      
      @output.puts("#{timestamp} [#{level.upcase}]#{context_str} #{message}")
    end
  end
end

# Usage across application
logger = ApplicationLogger.instance
logger.info("Application started")

logger.with_context("RequestID:abc123") do
  logger.info("Processing request")
  logger.error("Request failed")
end

The singleton logger provides consistent formatting, thread-safe output, and centralized log level control across all application components.

Cache Manager

In-memory caching benefits from singleton implementation to prevent cache duplication:

require 'singleton'

class CacheManager
  include Singleton
  
  def initialize
    @cache = {}
    @mutex = Mutex.new
    @ttl = {}
  end
  
  def set(key, value, expires_in: nil)
    @mutex.synchronize do
      @cache[key] = value
      @ttl[key] = Time.now + expires_in if expires_in
    end
  end
  
  def get(key)
    @mutex.synchronize do
      if @ttl[key] && Time.now > @ttl[key]
        delete(key)
        return nil
      end
      
      @cache[key]
    end
  end
  
  def delete(key)
    @mutex.synchronize do
      @cache.delete(key)
      @ttl.delete(key)
    end
  end
  
  def clear
    @mutex.synchronize do
      @cache.clear
      @ttl.clear
    end
  end
  
  def stats
    @mutex.synchronize do
      expired = @ttl.count { |_, expiry| Time.now > expiry }
      {
        total_keys: @cache.size,
        expired_keys: expired,
        active_keys: @cache.size - expired
      }
    end
  end
end

# Application usage
cache = CacheManager.instance
cache.set('user:123', { name: 'John' }, expires_in: 3600)
cache.set('session:abc', { token: 'xyz' }, expires_in: 300)

user = cache.get('user:123')
cache.stats  # => {:total_keys=>2, :expired_keys=>0, :active_keys=>2}

Design Considerations

When to Use Singleton Pattern

The singleton pattern suits specific scenarios where exactly one instance provides genuine value. Resource managers that coordinate access to limited resources (file handles, network connections, hardware devices) benefit from singleton enforcement. Configuration objects that load settings once and distribute them globally eliminate redundant file parsing and memory usage.

The pattern applies when instance proliferation causes problems: multiple database connection pools deplete available connections, multiple cache managers waste memory on duplicate data, or multiple logging instances create file access conflicts. Singleton prevents these issues by design.

State coordination across an application becomes simpler with singletons. When components need to share information about application status, user sessions, or feature flags, a singleton state manager eliminates passing references through deep call stacks.

When to Avoid Singleton Pattern

The pattern introduces global state, creating hidden dependencies between components. Code that relies on singleton instances becomes harder to test because tests share singleton state, requiring explicit cleanup between test cases. This coupling reduces modularity and makes code harder to understand.

Concurrency complexity increases with singletons holding mutable state. Thread-safe implementations require careful synchronization, adding performance overhead and potential deadlock scenarios. Applications with high concurrency demands often fare better with alternative approaches.

Testing becomes problematic because singleton state persists across tests. Each test should run independently, but shared singleton state creates test interdependencies. Mocking or stubbing singleton instances requires additional test infrastructure and ceremony.

# Testing challenge with singletons
class ServiceTests < Minitest::Test
  def setup
    # Need to explicitly reset singleton state
    if AppConfig.instance_variable_defined?(:@instance)
      AppConfig.instance_variable_set(:@instance, nil)
    end
  end
  
  def test_service_with_config
    # Test runs with fresh singleton instance
    config = AppConfig.instance
    service = Service.new
    assert service.process
  end
end

Alternative Patterns

Dependency injection eliminates singleton's global state problems by passing instances through constructor parameters. This approach makes dependencies explicit and simplifies testing:

class Service
  def initialize(logger:, cache:)
    @logger = logger
    @cache = cache
  end
  
  def process
    @logger.info("Processing started")
    data = @cache.get(:data)
    # Process data
  end
end

# Explicit dependency injection
logger = Logger.new
cache = Cache.new
service = Service.new(logger: logger, cache: cache)

Module functions provide singleton-like behavior without class instantiation complexity:

module Statistics
  @data = []
  
  def self.add(value)
    @data << value
  end
  
  def self.mean
    return 0 if @data.empty?
    @data.sum.to_f / @data.size
  end
  
  def self.reset
    @data.clear
  end
end

Constants work well for immutable singleton data:

DATABASE_CONFIG = {
  host: 'localhost',
  port: 5432,
  database: 'production'
}.freeze

# Usage throughout application
connection = connect(DATABASE_CONFIG)

Thread-local storage provides singleton-like behavior scoped to individual threads:

class RequestContext
  def self.current
    Thread.current[:request_context] ||= new
  end
  
  attr_accessor :request_id, :user_id
  
  def initialize
    @request_id = SecureRandom.uuid
  end
end

# Each thread gets its own instance
Thread.new do
  context = RequestContext.current
  context.user_id = 123
end

Design Trade-offs

Singleton patterns trade flexibility for simplicity. Global access simplifies code by eliminating parameter passing, but creates implicit dependencies that complicate testing and maintenance. The pattern suits applications where the simplified access patterns outweigh testing complexity.

Performance characteristics favor singleton when object creation carries significant overhead. Loading configuration files, establishing database connections, or initializing large data structures benefit from single instantiation. However, thread synchronization overhead may offset these benefits in highly concurrent scenarios.

Scalability implications arise in distributed systems. Singleton patterns assume single-process operation and break down across multiple processes or servers. Distributed caching, configuration management, or resource coordination requires different approaches like distributed locks or coordination services.

Common Patterns

Lazy Initialization Pattern

The standard lazy initialization pattern defers instance creation until first access:

class ResourceManager
  @instance = nil
  @mutex = Mutex.new
  
  def self.instance
    return @instance if @instance
    
    @mutex.synchronize do
      @instance ||= new
    end
  end
  
  private_class_method :new
  
  def initialize
    # Expensive initialization
    @resources = allocate_resources
  end
  
  private
  
  def allocate_resources
    # Simulated expensive operation
    sleep(0.5)
    { allocated: true, count: 10 }
  end
end

This pattern minimizes startup time and memory usage when singletons remain unused during certain execution paths. The double-checked locking (checking before and after synchronization) reduces lock contention after initialization.

Eager Initialization Pattern

Eager initialization creates instances at class load time, eliminating lazy loading complexity:

class SystemMonitor
  INSTANCE = new
  
  def self.instance
    INSTANCE
  end
  
  private_class_method :new
  
  def initialize
    @start_time = Time.now
    @metrics = {}
  end
  
  def uptime
    Time.now - @start_time
  end
  
  def record_metric(name, value)
    @metrics[name] = value
  end
end

This approach eliminates thread safety concerns during initialization and guarantees instance availability. Use eager initialization when initialization cost remains low and the singleton always gets used during application execution.

Registry Pattern with Singleton

The registry pattern manages multiple named singleton instances:

class DatabaseConnectionRegistry
  @connections = {}
  @mutex = Mutex.new
  
  def self.get(name)
    @mutex.synchronize do
      @connections[name] ||= create_connection(name)
    end
  end
  
  def self.close_all
    @mutex.synchronize do
      @connections.each_value(&:close)
      @connections.clear
    end
  end
  
  private
  
  def self.create_connection(name)
    # Create connection based on name
    config = load_config(name)
    Connection.new(config)
  end
  
  def self.load_config(name)
    # Load configuration for named connection
    { name: name, host: 'localhost', port: 5432 }
  end
end

# Usage
primary = DatabaseConnectionRegistry.get(:primary)
replica = DatabaseConnectionRegistry.get(:replica)

This pattern provides singleton benefits while allowing multiple instances differentiated by keys. Each named instance remains a singleton within its namespace.

Singleton with Cloning Prevention

Ruby objects can be cloned or duplicated by default. Singleton implementations should prevent cloning:

require 'singleton'

class SecureConfig
  include Singleton
  
  attr_reader :api_key
  
  def initialize
    @api_key = 'secret_key_12345'
  end
  
  private
  
  def clone
    raise TypeError, "Cannot clone singleton"
  end
  
  def dup
    raise TypeError, "Cannot duplicate singleton"
  end
end

config = SecureConfig.instance

begin
  config.clone
rescue TypeError => e
  puts e.message  # => "Cannot clone singleton"
end

The Singleton module automatically prevents cloning and duplication, but manual implementations should include these protections.

Multiton Pattern

The multiton pattern extends singleton to manage a fixed number of named instances:

class DatabaseConnection
  @instances = {}
  @mutex = Mutex.new
  @max_instances = 3
  
  def self.instance(name)
    @mutex.synchronize do
      if @instances[name].nil?
        raise "Max instances reached" if @instances.size >= @max_instances
        @instances[name] = new(name)
      end
      
      @instances[name]
    end
  end
  
  def self.instances
    @instances.keys
  end
  
  private_class_method :new
  
  attr_reader :name
  
  def initialize(name)
    @name = name
  end
end

# Create named instances
db1 = DatabaseConnection.instance(:primary)
db2 = DatabaseConnection.instance(:analytics)
db3 = DatabaseConnection.instance(:cache)

DatabaseConnection.instances  # => [:primary, :analytics, :cache]

Common Pitfalls

Thread Safety Violations

The most common singleton pitfall involves race conditions during instance creation in multi-threaded environments:

# INCORRECT: Not thread-safe
class UnsafeCache
  @instance = nil
  
  def self.instance
    @instance ||= new  # Race condition possible here
  end
  
  private_class_method :new
end

# Multiple threads might create multiple instances
threads = 10.times.map do
  Thread.new { UnsafeCache.instance }
end

instances = threads.map(&:value)
# instances might contain duplicate instances

Without proper synchronization, multiple threads executing the conditional assignment simultaneously can create multiple instances. This violates the singleton guarantee and leads to subtle bugs from unexpected instance proliferation.

The correct implementation requires mutex synchronization:

class SafeCache
  @instance = nil
  @mutex = Mutex.new
  
  def self.instance
    return @instance if @instance
    
    @mutex.synchronize do
      @instance ||= new
    end
  end
  
  private_class_method :new
end

Testing Difficulties

Singleton state persists across test cases, creating test interdependencies:

require 'singleton'

class Counter
  include Singleton
  
  attr_accessor :count
  
  def initialize
    @count = 0
  end
  
  def increment
    @count += 1
  end
end

# Test isolation problems
class CounterTest < Minitest::Test
  def test_first
    counter = Counter.instance
    counter.increment
    assert_equal 1, counter.count  # Passes
  end
  
  def test_second
    counter = Counter.instance
    # Expects 0, but count is 1 from previous test
    assert_equal 0, counter.count  # Fails due to shared state
  end
end

Solutions include explicitly resetting singleton state in test setup or using test doubles to replace singleton instances:

class CounterTest < Minitest::Test
  def setup
    # Reset singleton state before each test
    counter = Counter.instance
    counter.count = 0
  end
  
  def teardown
    # Alternative: reset singleton instance entirely
    Counter.instance_variable_set(:@singleton__instance__, nil)
  end
end

Global State Dependencies

Singletons create hidden dependencies that make code harder to understand and maintain:

class OrderProcessor
  def process(order)
    # Hidden dependency on singleton
    logger = Logger.instance
    cache = Cache.instance
    config = Config.instance
    
    logger.info("Processing order #{order.id}")
    
    if cache.get("order:#{order.id}")
      logger.warn("Duplicate order")
      return false
    end
    
    # Process order using config settings
    result = execute_order(order, config.settings)
    cache.set("order:#{order.id}", result)
    
    result
  end
end

These implicit dependencies complicate testing and make the code's requirements unclear. Explicit dependency injection improves clarity:

class OrderProcessor
  def initialize(logger:, cache:, config:)
    @logger = logger
    @cache = cache
    @config = config
  end
  
  def process(order)
    @logger.info("Processing order #{order.id}")
    
    if @cache.get("order:#{order.id}")
      @logger.warn("Duplicate order")
      return false
    end
    
    result = execute_order(order, @config.settings)
    @cache.set("order:#{order.id}", result)
    
    result
  end
end

Inheritance Issues

Singleton behavior does not propagate naturally through inheritance:

require 'singleton'

class BaseStore
  include Singleton
  
  def save(data)
    puts "Saving to base store"
  end
end

class UserStore < BaseStore
  def save(data)
    puts "Saving to user store"
  end
end

base = BaseStore.instance
user = UserStore.instance

# Two different singleton instances
base.object_id == user.object_id  # => false

Each class maintains its own singleton instance. Subclasses create separate singletons rather than sharing the parent's instance. This behavior often surprises developers expecting shared singleton instances across inheritance hierarchies.

Memory Leaks Through Singleton State

Singletons that accumulate data without cleanup create memory leaks:

class EventLog
  include Singleton
  
  def initialize
    @events = []
  end
  
  def log(event)
    # Unbounded array growth
    @events << { timestamp: Time.now, data: event }
  end
  
  def events
    @events
  end
end

# Memory grows indefinitely
100_000.times do |i|
  EventLog.instance.log("Event #{i}")
end

Implement size limits and cleanup strategies for singleton state:

class EventLog
  include Singleton
  
  MAX_EVENTS = 1000
  
  def initialize
    @events = []
  end
  
  def log(event)
    @events << { timestamp: Time.now, data: event }
    @events.shift if @events.size > MAX_EVENTS
  end
end

Serialization Problems

Singleton instances create complications during serialization and deserialization:

require 'singleton'

class AppState
  include Singleton
  
  attr_accessor :data
  
  def initialize
    @data = { initialized: true }
  end
end

# Serialization breaks singleton guarantee
state1 = AppState.instance
serialized = Marshal.dump(state1)
state2 = Marshal.load(serialized)

state1.object_id == state2.object_id  # => false

The Singleton module provides methods to handle serialization correctly, but custom implementations must implement proper serialization behavior manually.

Testing Approaches

Singleton Reset Between Tests

Tests require singleton state cleanup to maintain isolation:

require 'minitest/autorun'
require 'singleton'

class Database
  include Singleton
  
  attr_accessor :data
  
  def initialize
    @data = {}
  end
  
  def save(key, value)
    @data[key] = value
  end
end

class DatabaseTest < Minitest::Test
  def setup
    # Reset singleton state
    Database.instance.data.clear
  end
  
  def test_save
    db = Database.instance
    db.save(:user, 'John')
    assert_equal 'John', db.data[:user]
  end
  
  def test_empty_database
    db = Database.instance
    assert_empty db.data
  end
end

Dependency Injection for Testing

Replace singleton dependencies with test doubles:

class OrderService
  def initialize(logger: Logger.instance, cache: Cache.instance)
    @logger = logger
    @cache = cache
  end
  
  def process(order)
    @logger.info("Processing")
    @cache.set(order.id, order)
  end
end

class OrderServiceTest < Minitest::Test
  def test_process
    # Inject mock dependencies
    mock_logger = Minitest::Mock.new
    mock_cache = Minitest::Mock.new
    
    mock_logger.expect(:info, nil, ["Processing"])
    mock_cache.expect(:set, nil, [123, Object])
    
    service = OrderService.new(logger: mock_logger, cache: mock_cache)
    order = OpenStruct.new(id: 123)
    
    service.process(order)
    
    mock_logger.verify
    mock_cache.verify
  end
end

Testing Singleton Instance Methods

Focus tests on singleton behavior rather than instantiation mechanics:

require 'minitest/autorun'
require 'singleton'

class ConfigTest < Minitest::Test
  def setup
    @config = Config.instance
  end
  
  def test_singleton_instance
    config2 = Config.instance
    assert_same @config, config2
  end
  
  def test_get_setting
    assert_equal 'postgresql', @config.get(:database)
  end
  
  def test_set_setting
    @config.set(:feature_flag, true)
    assert @config.get(:feature_flag)
  end
  
  def teardown
    # Clean up settings changed during test
    @config.set(:feature_flag, nil)
  end
end

Mocking Singleton Responses

Override singleton instance methods temporarily for testing:

class ApiClient
  include Singleton
  
  def fetch_data
    # Actual API call
    Net::HTTP.get(URI('https://api.example.com/data'))
  end
end

class ServiceTest < Minitest::Test
  def test_with_mocked_api
    client = ApiClient.instance
    
    # Temporarily override method
    client.define_singleton_method(:fetch_data) do
      '{"status": "ok"}'
    end
    
    # Test code that uses ApiClient
    result = perform_operation
    assert_equal 'ok', JSON.parse(result)['status']
  end
  
  def perform_operation
    ApiClient.instance.fetch_data
  end
end

Reference

Singleton Implementation Comparison

Approach Thread Safe Lazy Init Complexity Use Case
Singleton module Yes Yes Low Standard singleton needs
Manual with mutex Yes Yes Medium Custom initialization control
Module-based N/A N/A Low Stateless utilities
Eager initialization Yes No Low Always-needed singletons
Class instance variable Depends Yes Medium Simple singleton requirements

Ruby Singleton Module Methods

Method Description Returns
instance Returns the singleton instance Singleton instance
clone Raises TypeError N/A
dup Raises TypeError N/A
_load Deserializes singleton Singleton instance
_dump Serializes singleton String

Singleton Pattern Characteristics

Characteristic Benefit Drawback
Single instance Resource efficiency Global state coupling
Global access Simplified access patterns Hidden dependencies
Lazy initialization Reduced startup cost Thread safety complexity
Persistent state Data sharing Testing difficulty
Controlled instantiation Prevents misuse Inheritance complications

Thread Safety Techniques

Technique Description Overhead
Mutex synchronization Locks during instance creation Medium
Double-checked locking Checks before and after lock Low
Eager initialization Creates at load time None
Ruby Singleton module Built-in thread safety Low
Class variables Atomic assignment in CRuby None

Common Singleton Use Cases

Use Case Rationale Example
Configuration management Single source of settings Application config
Connection pools Resource coordination Database connections
Logging Centralized output Application logger
Cache management Unified cache storage In-memory cache
State coordination Shared state access Feature flags
Hardware interfaces Device exclusivity Printer driver

Anti-Patterns to Avoid

Anti-Pattern Problem Solution
Singleton for convenience Artificial global state Use dependency injection
Testing without cleanup State leaks between tests Reset singleton in setup
Mutable global state Concurrency issues Use immutable data or locks
Singleton inheritance Multiple instances created Composition over inheritance
No size limits Memory leaks Implement data retention policies
Ignored thread safety Race conditions Use mutex or Singleton module

Testing Strategies

Strategy Implementation When to Use
State reset Clear singleton data in setup Simple state objects
Instance replacement Stub singleton instance Complex behaviors
Dependency injection Pass instances via constructor All new code
Test isolation Reset singleton instance variable Legacy code
Method mocking Override singleton methods API calls, external services