Skip to content

Singleton Scope

The singleton scope creates one instance of a service that lives for the entire application lifetime. It's the default scope in InjectQ and is perfect for shared resources.

๐ŸŽฏ What is Singleton Scope?

A singleton service is instantiated once and the same instance is returned for all subsequent requests.

from injectq import InjectQ, singleton

container = InjectQ()

@singleton
class Database:
    def __init__(self):
        self.connection_id = id(self)
        print(f"Database created: {self.connection_id}")

# Register and use
container.bind(Database, Database)

# First access creates instance
db1 = container.get(Database)
print(f"First instance: {db1.connection_id}")

# Subsequent accesses return same instance
db2 = container.get(Database)
print(f"Second instance: {db2.connection_id}")
print(f"Same instance? {db1 is db2}")  # True

๐Ÿ—๏ธ When to Use Singleton

โœ… Perfect For

  • Database connections - Share connection pool
  • Configuration objects - App-wide settings
  • Caching services - Shared cache instance
  • Logging services - Centralized logging
  • Expensive resources - Services with high creation cost
@singleton
class DatabaseConnection:
    """โœ… Good - shared connection pool"""
    def __init__(self):
        self.pool = create_connection_pool()

@singleton
class AppConfig:
    """โœ… Good - application configuration"""
    def __init__(self):
        self.database_url = os.getenv("DATABASE_URL")
        self.redis_url = os.getenv("REDIS_URL")

@singleton
class RedisCache:
    """โœ… Good - shared cache"""
    def __init__(self, redis_url: str):
        self.client = redis.Redis.from_url(redis_url)

โŒ Avoid For

  • Request-specific data - Use scoped instead
  • User session data - Use scoped instead
  • Temporary state - Use transient instead
@singleton
class UserSession:
    """โŒ Bad - user-specific data gets mixed up"""
    def __init__(self):
        self.user_id = None
        self.permissions = []

@singleton
class RequestContext:
    """โŒ Bad - request data gets overwritten"""
    def __init__(self):
        self.request_id = None
        self.start_time = None

๐Ÿ”ง Creating Singletons

Decorator Approach

from injectq import singleton

@singleton
class Database:
    def __init__(self, config: DatabaseConfig):
        self.config = config
        self.connection = create_connection(config.url)

# Automatic registration with container
container = InjectQ()
db = container.get(Database)  # Works automatically

Explicit Binding

from injectq import Scope

# Explicit binding with scope
container.bind(Database, Database, scope=Scope.SINGLETON)

# Or with string
container.bind(Database, Database, scope="singleton")

Factory Function

def create_database() -> Database:
    config = load_config()
    return Database(config)

container.bind_factory(Database, create_database)
# Result is still singleton (cached after first creation)

๐ŸŽจ Singleton Patterns

Lazy Initialization

Singletons are created lazily - only when first requested:

@singleton
class ExpensiveService:
    def __init__(self):
        print("Creating expensive service...")
        time.sleep(2)  # Simulate expensive initialization

print("Container ready")
# Service not created yet

service = container.get(ExpensiveService)
# "Creating expensive service..." printed here

service2 = container.get(ExpensiveService)
# No second creation - same instance returned

Singleton with Dependencies

@singleton
class Database:
    def __init__(self, config: DatabaseConfig):
        self.config = config

@singleton
class UserRepository:
    def __init__(self, db: Database):
        self.db = db

@singleton
class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo

# Dependency chain:
# UserService -> UserRepository -> Database -> DatabaseConfig
# All are singletons, so the chain is created once

Singleton Registry

@singleton
class ServiceRegistry:
    def __init__(self):
        self.services = {}

    def register(self, name: str, service):
        self.services[name] = service

    def get(self, name: str):
        return self.services.get(name)

# Usage
registry = container.get(ServiceRegistry)
registry.register("email", EmailService())

โšก Performance Benefits

Memory Efficiency

@singleton
class SharedCache:
    def __init__(self):
        self.data = {}  # One dictionary for entire app

# vs

@transient
class IndividualCache:
    def __init__(self):
        self.data = {}  # New dictionary each time

Creation Cost

@singleton
class DatabaseConnection:
    def __init__(self):
        # Expensive operation - done once
        self.pool = create_connection_pool(max_size=20)

# First access: ~2 seconds
# Subsequent accesses: ~0.001 seconds

Reference Equality

@singleton
class AppConfig:
    pass

config1 = container.get(AppConfig)
config2 = container.get(AppConfig)

# Can use identity comparison
if config1 is config2:
    print("Same config object")

๐Ÿงช Testing Singletons

Testing Singleton Behavior

def test_singleton_behavior():
    with test_container() as container:
        container.bind(Database, Database, scope="singleton")

        # Should be same instance
        db1 = container.get(Database)
        db2 = container.get(Database)
        assert db1 is db2

        # Test the singleton
        db1.connect()
        assert db2.is_connected()

Overriding Singletons

def test_with_mock_singleton():
    mock_db = MockDatabase()

    with override_dependency(Database, mock_db):
        # All code sees the mock
        service = container.get(UserService)
        result = service.get_user(1)
        assert result.name == "Mock User"

Resetting Singletons

# For testing - reset singleton instances
container.clear_scope("singleton")

# Or reset entire container
container.clear()

๐Ÿšจ Thread Safety

Singletons must be thread-safe if used in multi-threaded environments:

@singleton
class ThreadSafeCache:
    def __init__(self):
        self._data = {}
        self._lock = threading.Lock()

    def get(self, key: str):
        with self._lock:
            return self._data.get(key)

    def set(self, key: str, value):
        with self._lock:
            self._data[key] = value

# Usage in multi-threaded app
cache = container.get(ThreadSafeCache)
cache.set("user_123", user_data)

๐Ÿ”„ Singleton Lifecycle

Creation

print("1. Container created")
container = InjectQ()

print("2. Service registered")
container.bind(Database, Database)

print("3. First access triggers creation")
db = container.get(Database)  # Database.__init__ called here

print("4. Subsequent accesses use cached instance")
db2 = container.get(Database)  # No creation

Cleanup

# Manual cleanup
container.clear_scope("singleton")

# Or clear all
container.clear()

# Singletons are garbage collected when container is deleted
del container

๐Ÿ† Best Practices

1. Use for Shared Resources

@singleton
class DatabaseConnection:
    """โœ… Shared database connection"""
    pass

@singleton
class RedisClient:
    """โœ… Shared Redis connection"""
    pass

@singleton
class AppConfig:
    """โœ… Shared configuration"""
    pass

2. Ensure Thread Safety

@singleton
class SharedService:
    def __init__(self):
        self._lock = threading.Lock()

    def do_work(self):
        with self._lock:
            # Thread-safe operations
            pass

3. Avoid Mutable State Issues

@singleton
class UserManager:
    def __init__(self):
        self.current_user = None  # โŒ Mutable state

    def set_current_user(self, user):
        self.current_user = user  # โŒ Overwrites for all users

# โœ… Use scoped or transient instead
@scoped("request")
class RequestUser:
    def __init__(self):
        self.user = None

    def set_user(self, user):
        self.user = user  # โœ… Unique per request

4. Document Singleton Usage

@singleton
class MetricsCollector:
    """Application-wide metrics collection.

    This service collects metrics across all requests.
    Thread-safe for concurrent access.
    """
    pass

5. Use Factories for Complex Setup

def create_database_pool() -> DatabasePool:
    """Factory for complex database setup."""
    config = load_database_config()
    pool = create_connection_pool(config)
    setup_connection_monitoring(pool)
    return pool

container.bind_factory(DatabasePool, create_database_pool)

๐Ÿšจ Common Singleton Mistakes

1. Storing Request Data

@singleton
class RequestCache:
    def __init__(self):
        self.data = {}  # โŒ Shared across requests

    def set_request_data(self, request_id, data):
        self.data[request_id] = data  # โŒ Race conditions

2. Not Handling Thread Safety

@singleton
class Counter:
    def __init__(self):
        self.count = 0  # โŒ Not thread-safe

    def increment(self):
        self.count += 1  # โŒ Race conditions in multi-threaded apps

3. Expensive Initialization in Constructor

@singleton
class Service:
    def __init__(self):
        # โŒ Expensive work in constructor blocks app startup
        self.data = load_large_dataset()
        self.model = train_ml_model()

๐ŸŽฏ Summary

Singleton scope provides:

  • One instance for the entire application
  • Memory efficient for shared resources
  • Performance optimized with caching
  • Lazy initialization - created only when needed
  • Thread safety concerns must be handled

Perfect for: - Database connections and pools - Configuration objects - Caching services - Logging and monitoring - Expensive shared resources

Key principles: - Use for truly shared, application-wide resources - Ensure thread safety in multi-threaded environments - Avoid storing request-specific or user-specific data - Consider lazy initialization for expensive resources - Document thread safety guarantees

Ready to explore transient scope?