Scope Best Practices¶
Scope best practices guide you to choose the right scope for each service and avoid common pitfalls in dependency injection.
๐ฏ Choosing the Right Scope¶
Decision Framework¶
def choose_scope(service_class):
"""
Framework for choosing the right scope
"""
# 1. Is the service stateless?
if service_has_no_state(service_class):
return "transient" # โ
New instance each time
# 2. Is the service shared across the entire application?
if service_is_global(service_class):
return "singleton" # โ
One instance for all
# 3. Is the service specific to a request/session/workflow?
if service_is_context_specific(service_class):
return "scoped" # โ
One instance per context
# 4. Does the service need custom lifetime rules?
if service_needs_custom_lifetime(service_class):
return "custom" # โ
Define your own rules
# Default to scoped for safety
return "scoped"
Quick Reference¶
Service Type | State | Sharing | Lifetime | Recommended Scope |
---|---|---|---|---|
Validators | Stateless | Per operation | Short | Transient |
Controllers | Stateless | Per request | Short | Transient |
Repositories | Stateless | Per operation | Short | Transient |
Database Connections | Stateful | Per request | Medium | Scoped |
User Sessions | Stateful | Per user | Long | Custom (Session) |
Caches | Stateful | Per request | Medium | Scoped |
Configurations | Stateful | Global | App lifetime | Singleton |
Loggers | Stateless | Per operation | Short | Transient |
Email Services | Stateless | Per operation | Short | Transient |
๐๏ธ Scope Selection Guidelines¶
Transient Scope Guidelines¶
Use transient for: - Stateless services - No instance variables - Lightweight operations - Fast to create - Isolated operations - No shared state needed - Validation logic - Check data without storing - Data transformation - Process without persistence
# โ
Good transient services
@transient
class EmailValidator:
def validate(self, email: str) -> bool:
return "@" in email and "." in email
@transient
class DataProcessor:
def process(self, data: dict) -> dict:
return {k: v.upper() for k, v in data.items()}
@transient
class PasswordHasher:
def hash(self, password: str) -> str:
return bcrypt.hashpw(password.encode(), bcrypt.gensalt())
Avoid transient for: - Expensive resources - Database connections - Shared state - Caches, sessions - Heavy initialization - Loading large datasets
Singleton Scope Guidelines¶
Use singleton for: - Global resources - Database connection pools - Application configuration - Settings, constants - Shared caches - Application-wide caching - Heavy objects - Expensive to create - Static data - Reference data, lookup tables
# โ
Good singleton services
@singleton
class DatabasePool:
def __init__(self):
self.pool = create_connection_pool()
@singleton
class AppConfig:
def __init__(self):
self.database_url = os.getenv("DATABASE_URL")
self.redis_url = os.getenv("REDIS_URL")
@singleton
class GlobalCache:
def __init__(self):
self.cache = RedisCache()
Avoid singleton for: - Request-specific data - User sessions - Mutable state - Per-user preferences - Test isolation - Makes testing harder
Scoped Scope Guidelines¶
Use scoped for: - Request context - Per-request data - User sessions - Per-user state - Database transactions - Per-request transactions - Audit trails - Per-request logging - Temporary caches - Request-scoped caching
# โ
Good scoped services
@scoped
class RequestContext:
def __init__(self):
self.user_id = None
self.request_id = str(uuid.uuid4())
@scoped
class DatabaseTransaction:
def __init__(self, db_pool: DatabasePool):
self.transaction = db_pool.begin_transaction()
@scoped
class RequestCache:
def __init__(self):
self.cache = {}
Avoid scoped for: - Global state - Application-wide data - Stateless operations - No state to share - Long-running contexts - Memory accumulation
Custom Scope Guidelines¶
Use custom scopes for: - User sessions - Per-user lifetime - Tenant isolation - Per-tenant services - Workflow contexts - Per-workflow state - Batch operations - Per-batch lifetime - Feature contexts - Per-feature state
# โ
Good custom scope usage
class UserSessionManager(ScopeManager):
"""Per-user session scope"""
class TenantScopeManager(ScopeManager):
"""Per-tenant isolation scope"""
class WorkflowScopeManager(ScopeManager):
"""Per-workflow execution scope"""
Avoid custom scopes for: - Simple cases - Built-in scopes suffice - Over-engineering - Unnecessary complexity
๐จ Common Scope Mistakes¶
1. Wrong Scope Selection¶
# โ Bad: Singleton for per-request data
@singleton
class UserPreferences:
def __init__(self):
self.user_id = None # Only one user globally!
self.theme = "light"
# โ
Good: Scoped for per-request data
@scoped
class UserPreferences:
def __init__(self, user_id: int):
self.user_id = user_id
self.theme = load_user_theme(user_id)
2. Memory Leaks with Scoped¶
# โ Bad: Long-lived scoped services
@scoped
class WebSocketConnection:
def __init__(self):
self.messages = [] # Accumulates forever!
def handle_message(self, message):
self.messages.append(message) # Memory leak!
# โ
Good: Proper cleanup
@scoped
class WebSocketConnection:
def __init__(self):
self.messages = []
self.max_messages = 100
def handle_message(self, message):
self.messages.append(message)
if len(self.messages) > self.max_messages:
self.messages.pop(0) # Prevent unbounded growth
3. Singleton State Pollution¶
# โ Bad: Mutable singleton state
@singleton
class GlobalCounter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
# Tests interfere with each other!
def test_counter():
counter = container.get(GlobalCounter)
counter.increment()
assert counter.count == 1 # Fails if other tests ran first
# โ
Good: Immutable or reset-able singleton
@singleton
class GlobalCounter:
def __init__(self):
self._count = 0
def increment(self):
self._count += 1
return self._count
def get_count(self):
return self._count
# For testing
def reset(self):
self._count = 0
4. Transient Performance Issues¶
# โ Bad: Expensive transient
@transient
class DataLoader:
def __init__(self):
self.data = load_large_dataset() # 100MB! Created every time
def get_data(self, key):
return self.data.get(key)
# โ
Good: Move expensive part to singleton
@singleton
class DataCache:
def __init__(self):
self.data = load_large_dataset() # Loaded once
@transient
class DataLoader:
def __init__(self, cache: DataCache):
self.cache = cache
def get_data(self, key):
return self.cache.data.get(key)
5. Scope Confusion¶
# โ Bad: Mixed scope usage
@singleton
class UserManager:
def __init__(self):
self.current_user = None # โ Global state
# โ
Good: Clear separation
@singleton
class UserRepository:
def get_user(self, user_id: int) -> User:
return self.db.get(user_id)
@scoped
class UserContext:
def __init__(self, user_repo: UserRepository):
self.user_repo = user_repo
self.current_user = None
def set_user(self, user_id: int):
self.current_user = self.user_repo.get_user(user_id)
๐ Best Practices¶
1. Default to Scoped¶
# โ
Safe default
@scoped
class SafeService:
"""Use scoped unless you have a good reason not to"""
pass
# Only use other scopes when necessary
@singleton
class NecessarySingleton:
"""Only when global sharing is required"""
pass
2. Document Scope Decisions¶
@scoped
class UserSession:
"""
Scoped: Per-request user session data.
This service maintains user session state for the duration
of a single request. Each request gets its own session
instance, ensuring isolation between concurrent requests.
Dependencies: Requires IUserRepository for user loading.
Thread Safety: Safe due to request isolation.
"""
pass
@singleton
class DatabasePool:
"""
Singleton: Application-wide database connection pool.
This service provides a shared pool of database connections
that can be used across the entire application. The pool
is created once at startup and reused for all database
operations.
Performance: Reduces connection overhead.
Thread Safety: Pool handles concurrent access.
"""
pass
3. Test Scope Behavior¶
def test_scope_isolation():
"""Ensure scoped services are properly isolated"""
@scoped
class TestService:
def __init__(self):
self.id = str(uuid.uuid4())
# Test different scopes get different instances
with container.scope() as scope1:
svc1 = scope1.get(TestService)
with container.scope() as scope2:
svc2 = scope2.get(TestService)
assert svc1.id != svc2.id
def test_singleton_sharing():
"""Ensure singletons are shared correctly"""
@singleton
class TestSingleton:
def __init__(self):
self.id = str(uuid.uuid4())
svc1 = container.get(TestSingleton)
svc2 = container.get(TestSingleton)
assert svc1.id == svc2.id
assert svc1 is svc2
4. Monitor Scope Usage¶
class ScopeMonitor:
"""Monitor scope usage and performance"""
def __init__(self):
self.metrics = {
"transient_created": 0,
"scoped_active": 0,
"singleton_count": 0
}
def track_transient_creation(self, service_class):
self.metrics["transient_created"] += 1
def track_scoped_creation(self, service_class):
self.metrics["scoped_active"] += 1
def track_singleton_access(self, service_class):
self.metrics["singleton_count"] += 1
# Integrate with container
monitor = ScopeMonitor()
# Track in your services
@transient
class MonitoredTransient:
def __init__(self):
monitor.track_transient_creation(self.__class__)
5. Handle Scope Transitions¶
# Handle scope transitions gracefully
def migrate_from_singleton_to_scoped():
"""
Example: Migrating UserPreferences from singleton to scoped
"""
# Old singleton (problematic)
@singleton
class OldUserPreferences:
def __init__(self):
self.preferences = {} # Global state
# New scoped (better)
@scoped
class NewUserPreferences:
def __init__(self, user_id: int):
self.user_id = user_id
self.preferences = load_user_preferences(user_id)
# Migration wrapper
class UserPreferencesAdapter:
def __init__(self, old_prefs: OldUserPreferences, new_prefs: NewUserPreferences):
self.old = old_prefs
self.new = new_prefs
def get_preference(self, key: str) -> Any:
# Try new scoped first, fall back to old singleton
if hasattr(self.new, key):
return getattr(self.new, key)
return self.old.preferences.get(key, None)
โก Performance Optimization¶
1. Scope-Aware Caching¶
# Different caches for different scopes
@singleton
class GlobalCache:
"""Application-wide cache"""
def __init__(self):
self.cache = RedisCache()
@scoped
class RequestCache:
"""Per-request cache"""
def __init__(self):
self.cache = {}
@transient
class NoCache:
"""No caching - always fresh"""
pass
# Smart cache service
@transient
class SmartCache:
def __init__(self, global_cache: GlobalCache, request_cache: RequestCache):
self.global_cache = global_cache
self.request_cache = request_cache
def get(self, key: str):
# Try request cache first
value = self.request_cache.cache.get(key)
if value is not None:
return value
# Try global cache
value = self.global_cache.cache.get(key)
if value is not None:
# Promote to request cache
self.request_cache.cache[key] = value
return value
return None
2. Lazy Initialization¶
# Lazy singleton initialization
class LazySingleton:
_instance = None
_lock = threading.Lock()
@classmethod
def get_instance(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
# Usage in DI
@singleton
class LazyService:
def __init__(self):
# Expensive initialization
self.data = self._load_expensive_data()
def _load_expensive_data(self):
# Only called when first instance is created
time.sleep(5) # Simulate expensive operation
return {"loaded": True}
3. Scope Pooling¶
# Pool scoped instances for performance
class ScopedPool:
def __init__(self, factory, max_size: int = 10):
self.factory = factory
self.max_size = max_size
self.pool = {}
def get_for_scope(self, scope_id: str):
if scope_id not in self.pool:
if len(self.pool) >= self.max_size:
# Remove oldest
oldest = next(iter(self.pool))
del self.pool[oldest]
self.pool[scope_id] = self.factory()
return self.pool[scope_id]
def cleanup_scope(self, scope_id: str):
if scope_id in self.pool:
del self.pool[scope_id]
๐งช Testing Strategies¶
1. Scope-Specific Tests¶
def test_transient_isolation():
"""Test transient services are isolated"""
@transient
class TestTransient:
def __init__(self):
self.id = str(uuid.uuid4())
with test_container() as container:
svc1 = container.get(TestTransient)
svc2 = container.get(TestTransient)
assert svc1.id != svc2.id
def test_singleton_sharing():
"""Test singleton services are shared"""
@singleton
class TestSingleton:
def __init__(self):
self.id = str(uuid.uuid4())
with test_container() as container:
svc1 = container.get(TestSingleton)
svc2 = container.get(TestSingleton)
assert svc1.id == svc2.id
assert svc1 is svc2
def test_scoped_isolation():
"""Test scoped services are properly isolated"""
@scoped
class TestScoped:
def __init__(self):
self.id = str(uuid.uuid4())
with test_container() as container:
with container.scope() as scope1:
svc1a = scope1.get(TestScoped)
svc1b = scope1.get(TestScoped)
assert svc1a.id == svc1b.id # Same scope
with container.scope() as scope2:
svc2 = scope2.get(TestScoped)
assert svc2.id != svc1a.id # Different scope
2. Mocking Strategies¶
def test_with_mocked_scopes():
"""Test with mocked scoped dependencies"""
mock_cache = MockRequestCache()
with override_dependency(RequestCache, mock_cache):
with container.scope() as scope:
service = scope.get(MyService)
# Service gets the mock
assert service.cache is mock_cache
def test_scope_lifecycle():
"""Test scope creation and destruction"""
creation_count = 0
destruction_count = 0
@scoped
class LifecycleService:
def __init__(self):
nonlocal creation_count
creation_count += 1
def __del__(self):
nonlocal destruction_count
destruction_count += 1
with test_container() as container:
with container.scope() as scope:
service = scope.get(LifecycleService)
assert creation_count == 1
# Scope exited, service should be cleaned up
assert destruction_count == 1
3. Performance Testing¶
def test_scope_performance():
"""Test scope creation performance"""
@transient
class FastService:
def __init__(self):
pass
@scoped
class ScopedService:
def __init__(self):
pass
with test_container() as container:
# Test transient creation speed
start = time.time()
for _ in range(1000):
container.get(FastService)
transient_time = time.time() - start
# Test scoped creation speed
start = time.time()
for _ in range(1000):
with container.scope() as scope:
scope.get(ScopedService)
scoped_time = time.time() - start
# Scoped should be reasonably fast
assert scoped_time < transient_time * 2
๐ฏ Summary¶
Scope best practices:
- Choose wisely - Match scope to service lifetime needs
- Default to scoped - Safe choice for most services
- Document decisions - Explain why you chose each scope
- Test thoroughly - Verify isolation and sharing behavior
- Monitor usage - Track performance and memory usage
- Handle transitions - Migrate scopes when requirements change
Key principles: - Transient for stateless, lightweight operations - Singleton for global, expensive, shared resources - Scoped for request/session-specific state - Custom for domain-specific lifetime rules - Always test scope behavior and performance
Remember: Wrong scope choice can cause memory leaks, performance issues, or incorrect behavior. Choose carefully and test thoroughly!
Next: Ready to explore modules and providers?