Singleton

Imagine your Rails application needs to communicate with an external email service. To do this, it must first establish a secure connection, exchange credentials for an authentication token, and set up a connection pool. If every single controller action that sends an email performs this expensive set up by calling EmailClient.new, your response times will suffer dramatically under load. The Singleton pattern offers the solution here.

It acts as a gatekeeper, guaranteeing that the EmailClient is instantiated only once upon the application process’ first request, and then every subsequent part of the application uses that single, pre-configured, high-performance instance.

This pattern is a foundational creational design pattern that enforces a powerful architectural constraint: guaranteeing a class has only one instance throughout the application’s lifecycle while providing a global, singular access point to it.

In the multi-threaded environment of a Rails application, this pattern is not used for business logic, but becomes a strategic necessity for performance and control over shared resources.

This necessity arises when dealing with two core issues:

  1. Enforced Immutability for Shared Data: When managing application settings or feature flags that must be a Single Source of Truth, the data must be immutable (read-only) once loaded. The Singleton serves as a controlled interface, using the Ruby #freeze method upon initialization to mitigate the primary risk of global state: unpredictable modification.
  2. Costly Resource Bootstrapping: For procedures with high startup overhead—like establishing a robust connection pool to an external microservice or parsing large configuration files—the Singleton ensures this expensive setup runs exactly once. By utilizing the lazy, memoized Ruby idiom @instance ||= new, it eliminates repetitive latency spikes associated with redundant initialization calls across numerous web workers, acting as a crucial performance optimization.

This pattern, cleanly implemented in Ruby using private_class_method :new to restrict direct instantiation, is highly effective for managing stateless utilities, centralized logging, and non-mutable configurations. While it introduces a tight coupling that requires careful testing, the gains in stability and performance for critical, shared resources often justify the trade-off.


The Core Problem: Costly Initialization and Conflicting State

Why use a Singleton?

Imagine a complex service that has a high startup cost, such as:

  1. Parsing a 5MB YAML configuration file.
  2. Establishing a pooled connection to an external metrics service.
  3. Generating a one-time cryptographic key for session signing.

If multiple parts of your application call Service.new on every request, you incur massive, repetitive overhead.

The Singleton pattern solves this by ensuring Lazy Initialization (it only runs the expensive setup once) and enforcing a Single Source of Truth, preventing competing instances from causing state conflicts.


The Ruby Singleton: Elegant and Explicit

The classic object-oriented implementation of the Singleton pattern involves three core steps, which the idiomatic Ruby approach achieves elegantly:

1. The Guard: Private Constructor

We use private_class_method :new to enforce the rule. This is the firewall preventing direct, uncontrolled instantiation. Any attempt to call AppConfig.new will result in a NoMethodError.

2. The Store: Class Instance Variable

We use the class instance variable @instance (not @@class_variable). This variable holds the reference to the single object.

3. The Accessor: The Memoized Getter

The self.instance method is the only legal way into the class. It uses the powerful memoization operator (||=) to handle the lazy initialization and storage simultaneously.

The Code Example (The Canonical Ruby Singleton)

# app/services/rate_limiter_config.rb
class RateLimiterConfig
  # 1. Block the standard constructor
  private_class_method :new 

  # 2. The Public Global Access Method
  def self.instance
    # 3. Lazy Initialization and Memoization
    # Only runs 'new' on the very first call.
    @instance ||= new 
  end
  # ... (Instance logic follows below in Use Case A)
end

Singleton Use Case A: Immutable Configuration Management

If your application relies on configuration data loaded from an external file or environment variables, the best practice is to load it once, freeze it, and access it globally. The Singleton enforces this single point of loading and access efficiently.

# Continuing RateLimiterConfig#initialize
  def initialize
    # Assume this involves reading external file, API call, etc.
    @limits = {
      'login' => 5,
      'checkout' => 20
    }.freeze # <-- CRITICAL: Prevents mutable global state
    Rails.logger.info 'Rate Limiter Configuration Loaded and Frozen.'
  end
  
  # Public Read-Only Access
  def max_attempts(endpoint)
    @limits.fetch(endpoint.to_s, 100)
  end
end

Usage

# In a controller or service:
# attempts = Redis.current.get(user_key)
# if attempts > RateLimiterConfig.instance.max_attempts(:login)
#   # ... block user
# end

As demonstrated by the reference: $RateLimiterConfig.instance.object\_id == RateLimiterConfig.instance.object\_id$ is guaranteed to be True.


Singleton Use Case B: The Stateless External Gateway

When integrating with a third-party service (e.g., Twilio, AWS S3), the client object often holds configuration (credentials, endpoints) but is functionally stateless—it simply formats and sends requests. Using a Singleton prevents redundant, costly client object creation.

AlternativeProblem / DrawbackSingleton Advantage
TwilioClient.new on every callHigh Overhead: Incurs repetitive costs like credential loading, establishing new TCP connections, and DNS lookup on every single request.Performance: Initializes the client object and its underlying connections exactly once at application boot.
Simple Ruby Module (Class Methods)Rigid Logic: Hard to manage complex initialization logic like connection pooling, or a lifecycle process (e.g., an OAuth token that needs to be generated once and refreshed every hour).Control: Provides a single, centralized point to manage the client’s internal state (like token expiry) and lifecycle efficiently.

Caveat Emptor: Testing and Global State

The critical caveat, which cannot be overstated, is testability. Because the Singleton instance persists globally, tests can become brittle.

When testing a component that relies on a Singleton:

  1. The test can be affected by a previous test that altered the Singleton’s state (if it were mutable).
  2. It’s difficult to mock or stub the singleton’s behaviour because the dependent class is tightly coupled to the concrete implementation.

Best Practice: Always treat the services wrapped by a Singleton as immutable and stateless to preserve test isolation and avoid the pitfalls of global state. Use Singletons sparingly and strategically in Rails.


References:

  1. https://refactoring.guru/design-patterns/singleton
  2. https://medium.com/@codified_brain/the-singleton-pattern-a-logical-guide-to-the-one-and-only-part-1-6632558f3767
  3. https://medium.com/@mehar.chand.cloud/singleton-design-pattern-use-case-config-management-158d6ad29f30
  4. https://www.mulesoft.com/resources/esb/what-is-single-source-of-truth-ssot

Stay tuned for the next blog from us – https://engineering.rently.com/

Leave a Reply

Login with