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 |