Skip to content

Binding Patterns

Binding patterns in InjectQ define how services are registered and resolved. Understanding these patterns is key to building flexible, maintainable applications.

๐ŸŽฏ Basic Binding

Instance Binding

Bind a specific instance to be reused:

from injectq import InjectQ

container = InjectQ()

# Bind a specific instance
config = AppConfig(host="prod", debug=False)
container.bind(AppConfig, config)

# Same instance returned every time
config1 = container.get(AppConfig)
config2 = container.get(AppConfig)
assert config1 is config2  # True

Class Binding

Bind a class for automatic instantiation:

class Database:
    def __init__(self, config: AppConfig):
        self.config = config

# Bind class - InjectQ creates instances as needed
container.bind(Database, Database)

# Each call creates a new instance (unless scoped)
db1 = container.get(Database)
db2 = container.get(Database)
assert db1 is not db2  # True (transient by default)

Factory Binding

Bind a factory function for custom creation logic:

def create_database(config: AppConfig) -> Database:
    if config.environment == "test":
        return SQLiteDatabase(config)
    else:
        return PostgreSQLDatabase(config)

container.bind_factory(Database, create_database)

# Factory called each time
db = container.get(Database)

๐Ÿ”ง Advanced Binding Patterns

Interface to Implementation

Bind abstractions to concrete implementations:

from typing import Protocol

class IDatabase(Protocol):
    def connect(self) -> None: ...
    def disconnect(self) -> None: ...

class PostgreSQLDatabase:
    def connect(self) -> None:
        print("Connected to PostgreSQL")

    def disconnect(self) -> None:
        print("Disconnected from PostgreSQL")

# Bind interface to implementation
container.bind(IDatabase, PostgreSQLDatabase)

# Usage
@inject
def use_database(db: IDatabase) -> None:
    db.connect()
    # ... use database
    db.disconnect()

Named Bindings

Multiple implementations of the same type:

class RedisCache:
    def __init__(self, host: str):
        self.host = host

class MemoryCache:
    def __init__(self):
        self.data = {}

# Named bindings
container.bind(Cache, RedisCache, name="redis")
container.bind(Cache, MemoryCache, name="memory")

# Resolve by name
redis_cache = container.get(Cache, name="redis")
memory_cache = container.get(Cache, name="memory")

Conditional Bindings

Bind different implementations based on conditions:

if environment == "production":
    container.bind(IDatabase, PostgreSQLDatabase)
    container.bind(ICache, RedisCache)
elif environment == "testing":
    container.bind(IDatabase, SQLiteDatabase)
    container.bind(ICache, MemoryCache)
else:
    container.bind(IDatabase, InMemoryDatabase)
    container.bind(ICache, MemoryCache)

Generic Bindings

Bind generic types:

from typing import TypeVar, Generic

T = TypeVar('T')

class Repository(Generic[T]):
    def __init__(self, entity_type: type):
        self.entity_type = entity_type

# Bind specific generic instances
container.bind(Repository[User], Repository[User])
container.bind(Repository[Order], Repository[Order])

# Usage
@inject
def get_user_repo(repo: Repository[User]) -> Repository[User]:
    return repo

๐ŸŽญ Scope-Based Bindings

Singleton Scope

One instance for the entire application:

from injectq import Scope

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

# Or use decorator
@singleton
class Database:
    pass

# Same instance everywhere
db1 = container.get(Database)
db2 = container.get(Database)
assert db1 is db2

Transient Scope

New instance every time:

from injectq import Scope, transient

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

# Or use decorator
@transient
class RequestHandler:
    pass

# Different instances
handler1 = container.get(RequestHandler)
handler2 = container.get(RequestHandler)
assert handler1 is not handler2

Scoped Bindings

Instance per scope (request, session, etc.):

from injectq import Scope, scoped

# Request-scoped
container.bind(UserSession, UserSession, scope=Scope.REQUEST)

# Or use decorator
@scoped("request")
class UserSession:
    pass

# Same instance within request scope
async with container.scope("request"):
    session1 = container.get(UserSession)
    session2 = container.get(UserSession)
    assert session1 is session2

# Different instance in new scope
async with container.scope("request"):
    session3 = container.get(UserSession)
    assert session1 is not session3

๐Ÿ“ฆ Module-Based Bindings

Simple Module

Group related bindings:

from injectq import Module

class DatabaseModule(Module):
    def configure(self, binder):
        binder.bind(IDatabase, PostgreSQLDatabase)
        binder.bind(DatabaseConfig, DatabaseConfig)

class ServiceModule(Module):
    def configure(self, binder):
        binder.bind(IUserService, UserService)
        binder.bind(IOrderService, OrderService)

# Use modules
container = InjectQ([DatabaseModule(), ServiceModule()])

Configuration Module

Bind configuration values:

from injectq import ConfigurationModule

config_module = ConfigurationModule({
    "database_url": "postgresql://localhost/db",
    "redis_url": "redis://localhost:6379",
    "app_name": "MyApp"
})

container = InjectQ([config_module])

# Access configuration
db_url = container.get(str, name="database_url")

Provider Module

Use providers for complex initialization:

from injectq import ProviderModule, provider

class CacheModule(ProviderModule):
    @provider
    def provide_cache(self, redis_url: str) -> ICache:
        return RedisCache(redis_url)

    @provider
    @singleton
    def provide_expensive_service(self, cache: ICache) -> ExpensiveService:
        return ExpensiveService(cache)

๐Ÿ”„ Binding Resolution Strategies

Type-Based Resolution

Default resolution by type:

class IUserRepository(Protocol):
    pass

class UserRepository:
    pass

container.bind(IUserRepository, UserRepository)

# Resolves UserRepository when IUserRepository is requested
repo = container.get(IUserRepository)  # Returns UserRepository instance

Name-Based Resolution

Resolve by name when multiple implementations exist:

container.bind(Cache, RedisCache, name="redis")
container.bind(Cache, MemoryCache, name="memory")

# Resolve by name
redis_cache = container.get(Cache, name="redis")
memory_cache = container.get(Cache, name="memory")

Context-Based Resolution

Different implementations based on context:

class DevelopmentDatabase:
    pass

class ProductionDatabase:
    pass

# Context-based binding
if os.getenv("ENV") == "production":
    container.bind(IDatabase, ProductionDatabase)
else:
    container.bind(IDatabase, DevelopmentDatabase)

๐Ÿงช Testing Binding Patterns

Override Bindings

from injectq.testing import override_dependency

def test_user_service():
    mock_repo = MockUserRepository()

    with override_dependency(IUserRepository, mock_repo):
        service = container.get(UserService)
        result = service.get_user(1)
        assert result.name == "Mock User"

Test Containers

from injectq.testing import test_container

def test_with_isolation():
    with test_container() as container:
        # Set up test bindings
        container.bind(IUserRepository, MockUserRepository)
        container.bind(IEmailService, MockEmailService)

        # Test the service
        service = container.get(UserService)
        user = service.create_user("test@example.com")
        assert user.email == "test@example.com"

Partial Overrides

def test_partial_override():
    # Override only some dependencies
    with override_dependency(ICache, MockCache):
        service = container.get(UserService)
        # service uses MockCache but real database
        pass

๐Ÿš€ Advanced Patterns

Decorator-Based Bindings

from injectq import singleton, transient, scoped

@singleton
class Database:
    pass

@transient
class RequestHandler:
    pass

@scoped("request")
class UserSession:
    pass

# Automatic registration when container starts
container = InjectQ()
# Decorated classes are automatically registered

Lazy Bindings

# Bind factory for lazy initialization
def create_expensive_service() -> ExpensiveService:
    print("Creating expensive service...")
    return ExpensiveService()

container.bind_factory(ExpensiveService, create_expensive_service)

# Service created only when first requested
print("Container ready")
service = container.get(ExpensiveService)  # "Creating expensive service..."

Conditional Factories

def create_cache(config: AppConfig) -> ICache:
    if config.use_redis:
        return RedisCache(config.redis_url)
    else:
        return MemoryCache()

container.bind_factory(ICache, create_cache)

โšก Performance Considerations

Binding Resolution

# Fast - direct type lookup
container.bind(IService, ServiceImpl)
service = container.get(IService)  # O(1) lookup

# Slower - factory invocation
container.bind_factory(IService, lambda: ServiceImpl())
service = container.get(IService)  # Factory execution overhead

Caching Strategies

# Singleton - cached after first creation
@singleton
class Database:
    def __init__(self):
        time.sleep(1)  # Expensive

db1 = container.get(Database)  # 1 second
db2 = container.get(Database)  # Instant (cached)

# Transient - no caching
@transient
class Handler:
    pass

h1 = container.get(Handler)  # New instance
h2 = container.get(Handler)  # New instance

๐Ÿ† Best Practices

1. Use Interfaces

# โœ… Good - depend on abstractions
container.bind(IDatabase, PostgreSQLDatabase)
container.bind(IUserService, UserService)

# โŒ Avoid - depend on concrete classes
container.bind(Database, PostgreSQLDatabase)
# โœ… Good - use modules
class DatabaseModule(Module):
    def configure(self, binder):
        binder.bind(IDatabase, PostgreSQLDatabase)
        binder.bind(DatabaseConfig, DatabaseConfig)

# โŒ Avoid - scattered bindings
container.bind(IDatabase, PostgreSQLDatabase)
container.bind(DatabaseConfig, DatabaseConfig)
container.bind(DatabaseConnection, DatabaseConnection)

3. Use Appropriate Scopes

# โœ… Good - correct scopes
@singleton
class Database:  # Shared resource
    pass

@scoped("request")
class UserSession:  # Per request
    pass

@transient
class EmailSender:  # Stateless
    pass

4. Document Complex Bindings

# โœ… Good - documented bindings
container.bind_factory(
    ICache,
    create_cache,
    # Redis cache for production, memory cache for testing
)

5. Validate Configuration

# โœ… Good - validate bindings
container = InjectQ([DatabaseModule(), ServiceModule()])
try:
    container.validate()
    print("โœ… All bindings valid")
except Exception as e:
    print(f"โŒ Binding error: {e}")
    exit(1)

๐Ÿšจ Common Binding Mistakes

1. Binding Concrete Classes

# โŒ Wrong - binding concrete class
container.bind(UserService, UserService)

# โœ… Correct - bind interface to implementation
container.bind(IUserService, UserService)

2. Wrong Scope

# โŒ Wrong - singleton for per-request data
@singleton
class RequestData:
    def __init__(self):
        self.user_id = None

# โœ… Correct - request-scoped
@scoped("request")
class RequestData:
    def __init__(self):
        self.user_id = None

3. Circular Dependencies

# โŒ Circular dependency
class A:
    def __init__(self, b: IB):
        self.b = b

class B:
    def __init__(self, a: IA):  # Circular!
        self.a = a

# โœ… Break circular dependency
class A:
    def __init__(self, b_factory: Callable[[], IB]):
        self.b_factory = b_factory

    def get_b(self) -> IB:
        return self.b_factory()

๐ŸŽฏ Summary

Binding patterns in InjectQ provide:

  • Flexible registration - Bind instances, classes, or factories
  • Type-based resolution - Automatic dependency resolution
  • Scope management - Control service lifetimes
  • Module organization - Group related bindings
  • Testing support - Easy dependency overrides

Key concepts: - Bind abstractions (interfaces/protocols) to implementations - Use appropriate scopes (singleton, transient, scoped) - Group bindings with modules - Validate configuration early - Use factories for complex initialization

Binding hierarchy: 1. Instance bindings - Specific objects 2. Class bindings - Automatic instantiation 3. Factory bindings - Custom creation logic 4. Module bindings - Organized groups 5. Decorator bindings - Automatic registration

Ready to explore scopes in detail?