Skip to content

Transient Scope

The transient scope creates a new instance of a service every time it's requested. It's perfect for stateless services and operations that need isolation.

๐ŸŽฏ What is Transient Scope?

A transient service creates a fresh instance for each request, ensuring no shared state between uses.

from injectq import InjectQ, transient

container = InjectQ()

@transient
class RequestHandler:
    def __init__(self):
        self.instance_id = id(self)
        self.created_at = time.time()
        print(f"Handler created: {self.instance_id}")

# Each access creates new instance
handler1 = container.get(RequestHandler)
time.sleep(0.1)
handler2 = container.get(RequestHandler)

print(f"Different instances: {handler1.instance_id != handler2.instance_id}")
print(f"Creation time difference: {handler2.created_at - handler1.created_at}")

๐Ÿ—๏ธ When to Use Transient

โœ… Perfect For

  • Request handlers - Process individual requests
  • Validators - Validate data without side effects
  • Command processors - Execute commands
  • Stateless services - No shared state needed
  • Data processors - Transform data without persistence
@transient
class EmailValidator:
    """โœ… Good - stateless validation"""
    def validate(self, email: str) -> bool:
        return "@" in email

@transient
class DataProcessor:
    """โœ… Good - processes data without storing state"""
    def process(self, data: dict) -> dict:
        return {"processed": True, **data}

@transient
class PasswordHasher:
    """โœ… Good - stateless hashing"""
    def hash(self, password: str) -> str:
        return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()

โŒ Avoid For

  • Database connections - Use singleton instead
  • Caching services - Use singleton instead
  • Shared resources - Use singleton instead
  • Expensive objects - Use singleton instead
@transient
class DatabaseConnection:
    """โŒ Bad - expensive to create repeatedly"""
    def __init__(self):
        self.conn = create_connection()  # Expensive!

@transient
class SharedCache:
    """โŒ Bad - cache should be shared"""
    def __init__(self):
        self.data = {}  # Lost on each creation

๐Ÿ”ง Creating Transients

Decorator Approach

from injectq import transient

@transient
class EmailSender:
    def __init__(self, smtp_config: SMTPConfig):
        self.config = smtp_config

    def send(self, to: str, subject: str, body: str):
        # Send email logic
        print(f"Sending email to {to}")

# Automatic registration
container = InjectQ()
sender = container.get(EmailSender)  # New instance each time

Explicit Binding

from injectq import Scope

# Explicit transient binding
container.bind(EmailSender, EmailSender, scope=Scope.TRANSIENT)

# Or with string
container.bind(EmailSender, EmailSender, scope="transient")

Factory Function

def create_validator() -> EmailValidator:
    # Custom creation logic
    validator = EmailValidator()
    validator.strict_mode = True
    return validator

container.bind_factory(EmailValidator, create_validator)
# Each call to factory creates new instance

๐ŸŽจ Transient Patterns

Stateless Operations

@transient
class UserValidator:
    def __init__(self, rules: ValidationRules):
        self.rules = rules

    def validate(self, user: User) -> ValidationResult:
        errors = []

        if not user.email:
            errors.append("Email is required")

        if len(user.password) < 8:
            errors.append("Password too short")

        return ValidationResult(valid=len(errors) == 0, errors=errors)

# Each validation gets fresh validator
validator1 = container.get(UserValidator)
validator2 = container.get(UserValidator)

result1 = validator1.validate(user1)
result2 = validator2.validate(user2)

Command Pattern

@transient
class CreateUserCommand:
    def __init__(self, user_repo: IUserRepository, event_bus: IEventBus):
        self.repo = user_repo
        self.event_bus = event_bus

    def execute(self, user_data: dict) -> User:
        user = User(**user_data)
        saved_user = self.repo.save(user)

        # Publish event
        self.event_bus.publish(UserCreatedEvent(saved_user.id))

        return saved_user

# Each command execution is isolated
command1 = container.get(CreateUserCommand)
command2 = container.get(CreateUserCommand)

user1 = command1.execute({"name": "Alice", "email": "alice@example.com"})
user2 = command2.execute({"name": "Bob", "email": "bob@example.com"})

Data Processing

@transient
class DataTransformer:
    def __init__(self, config: TransformConfig):
        self.config = config
        self.temp_files = []  # Instance-specific

    def transform(self, data: bytes) -> bytes:
        # Process data with temporary files
        temp_file = create_temp_file()
        self.temp_files.append(temp_file)

        # Transform logic
        result = process_data(data, temp_file, self.config)

        # Cleanup
        cleanup_temp_files(self.temp_files)

        return result

# Each transformation is isolated
transformer1 = container.get(DataTransformer)
transformer2 = container.get(DataTransformer)

result1 = transformer1.transform(data1)
result2 = transformer2.transform(data2)

โšก Performance Considerations

Creation Overhead

@transient
class SimpleProcessor:
    def __init__(self):
        pass  # Cheap

@transient
class ComplexProcessor:
    def __init__(self):
        self.data = load_large_dataset()  # Expensive!

# SimpleProcessor: Fast creation
# ComplexProcessor: Slow creation every time

Memory Usage

@transient
class LightProcessor:
    def __init__(self):
        self.buffer = bytearray(1024)  # Small

@transient
class HeavyProcessor:
    def __init__(self):
        self.data = bytearray(100 * 1024 * 1024)  # 100MB!

# Each request creates new instances
# HeavyProcessor: 100MB per request!

Garbage Collection

@transient
class FileProcessor:
    def __init__(self):
        self.temp_file = create_temp_file()

    def process(self):
        # Use temp file
        pass

    def cleanup(self):
        os.unlink(self.temp_file)

# Instances are garbage collected automatically
# But cleanup() might not be called

๐Ÿงช Testing Transients

Testing Isolation

def test_transient_isolation():
    with test_container() as container:
        container.bind(RequestHandler, RequestHandler, scope="transient")

        handler1 = container.get(RequestHandler)
        handler2 = container.get(RequestHandler)

        # Should be different instances
        assert handler1 is not handler2
        assert handler1.instance_id != handler2.instance_id

        # Test isolation
        handler1.state = "modified"
        assert not hasattr(handler2, 'state')

Mocking Dependencies

def test_with_mocked_dependencies():
    mock_repo = MockUserRepository()

    with override_dependency(IUserRepository, mock_repo):
        # Each transient gets the same mock dependency
        handler1 = container.get(RequestHandler)
        handler2 = container.get(RequestHandler)

        # Both use the same mock
        assert handler1.repo is mock_repo
        assert handler2.repo is mock_repo

Performance Testing

def test_creation_performance():
    start_time = time.time()

    # Create many transient instances
    for i in range(1000):
        handler = container.get(RequestHandler)

    end_time = time.time()
    avg_creation_time = (end_time - start_time) / 1000

    # Assert reasonable creation time
    assert avg_creation_time < 0.001  # Less than 1ms per instance

๐Ÿšจ Common Transient Mistakes

1. Expensive Initialization

@transient
class DatabaseProcessor:
    def __init__(self):
        # โŒ Expensive operation repeated
        self.schema = load_database_schema()
        self.cache = warm_up_cache()

    def process(self, query):
        # Use schema and cache
        pass

# โœ… Move expensive parts to dependencies
@singleton
class DatabaseSchema:
    def __init__(self):
        self.schema = load_database_schema()

@transient
class DatabaseProcessor:
    def __init__(self, schema: DatabaseSchema, cache: ICache):
        self.schema = schema
        self.cache = cache

2. Shared State Assumptions

@transient
class Counter:
    count = 0  # โŒ Class variable shared!

    def increment(self):
        self.count += 1
        return self.count

# All instances share the same count
counter1 = container.get(Counter)
counter2 = container.get(Counter)

counter1.increment()  # count = 1
counter2.increment()  # count = 2 (shared!)

# โœ… Use instance variables
@transient
class Counter:
    def __init__(self):
        self.count = 0  # โœ… Instance variable

    def increment(self):
        self.count += 1
        return self.count

3. Resource Leaks

@transient
class FileHandler:
    def __init__(self):
        self.file = open("temp.txt", "w")  # โŒ File not closed

    def write(self, data):
        self.file.write(data)

# Files accumulate without cleanup
for i in range(100):
    handler = container.get(FileHandler)
    handler.write(f"Data {i}")

# โœ… Use context managers or cleanup
@transient
class FileHandler:
    def __init__(self):
        self.file_path = create_temp_file()

    def __enter__(self):
        self.file = open(self.file_path, "w")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.file:
            self.file.close()
            os.unlink(self.file_path)

๐Ÿ† Best Practices

1. Keep Transients Lightweight

@transient
class LightweightValidator:
    def __init__(self, rules: ValidationRules):  # โœ… Simple dependencies
        self.rules = rules

    def validate(self, data):
        # โœ… Quick validation
        pass

2. Use for Stateless Operations

@transient
class PasswordEncoder:
    """โœ… Stateless - no shared state"""
    def encode(self, password: str) -> str:
        return bcrypt.hashpw(password.encode(), bcrypt.gensalt())

@transient
class JWTGenerator:
    """โœ… Stateless - generates new tokens"""
    def generate(self, user_id: int) -> str:
        return jwt.encode({"user_id": user_id}, SECRET_KEY)

3. Handle Resources Properly

@transient
class TempFileProcessor:
    def __init__(self):
        self.temp_file = None

    def __enter__(self):
        self.temp_file = tempfile.NamedTemporaryFile(delete=False)
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.temp_file:
            self.temp_file.close()
            os.unlink(self.temp_file.name)

# Usage
with container.get(TempFileProcessor) as processor:
    processor.process_data(data)

4. Document Transient Nature

@transient
class RequestProcessor:
    """Processes individual requests.

    This service is transient - a new instance is created
    for each request to ensure isolation and thread safety.
    """
    pass

5. Test Creation Performance

def test_transient_performance():
    """Ensure transient services are fast to create."""
    import time

    start = time.time()
    instances = [container.get(MyTransient) for _ in range(100)]
    end = time.time()

    avg_time = (end - start) / 100
    assert avg_time < 0.01  # Should be fast to create

๐Ÿ”„ Transient vs Singleton

Memory Usage

# Singleton - One instance
@singleton
class SharedService:
    def __init__(self):
        self.data = [1, 2, 3, 4, 5]  # 5 items

# Transient - New instance each time
@transient
class IsolatedService:
    def __init__(self):
        self.data = [1, 2, 3, 4, 5]  # 5 items per instance

# 100 requests:
# Singleton: 5 items total
# Transient: 500 items total

Thread Safety

# Singleton - Must be thread-safe
@singleton
class SharedCounter:
    def __init__(self):
        self._count = 0
        self._lock = threading.Lock()

    def increment(self):
        with self._lock:
            self._count += 1

# Transient - Automatically thread-safe
@transient
class IsolatedCounter:
    def __init__(self):
        self.count = 0  # No sharing

    def increment(self):
        self.count += 1  # Safe

State Management

# Singleton - Shared state
@singleton
class GlobalState:
    def __init__(self):
        self.current_user = None

# Transient - Isolated state
@transient
class RequestState:
    def __init__(self):
        self.current_user = None

๐ŸŽฏ Summary

Transient scope provides:

  • New instance for each request
  • Complete isolation between uses
  • Thread safety by default
  • Stateless operations support
  • Resource management challenges

Perfect for: - Request handlers and processors - Validators and data processors - Command execution - Stateless services - Operations needing isolation

Key principles: - Keep initialization cheap and fast - Avoid shared state between instances - Handle resource cleanup properly - Use for truly stateless operations - Test creation performance

Ready to explore scoped services?