Skip to content

Modules & Providers

Organize your dependency injection configuration into reusable, composable units.

What are Modules?

Modules are containers for related bindings that group services together.

from injectq import InjectQ
from injectq.modules import Module

class DatabaseModule(Module):
    def configure(self, binder):
        binder.bind(Database, PostgresDatabase)
        binder.bind(UserRepository, UserRepositoryImpl)

container = InjectQ.get_instance()
container.install_module(DatabaseModule())

# Services are now available
repo = container[UserRepository]

Module Types

1. Simple Module

Basic module for straightforward bindings:

from injectq.modules import SimpleModule

module = SimpleModule() \
    .bind(Database, PostgresDatabase) \
    .bind(Cache, RedisCache) \
    .bind_instance("api_key", "secret-123")

container.install_module(module)

2. Configuration Module

Bind configuration values:

from injectq.modules import ConfigurationModule

config = {
    "db_host": "localhost",
    "db_port": 5432,
    "api_key": "secret-key"
}

container.install_module(ConfigurationModule(config))

# Access configuration
host = container["db_host"]
port = container["db_port"]

3. Provider Module

Most powerful - use @provider methods for complex initialization:

from injectq.modules import ProviderModule, provider

class AppModule(ProviderModule):
    @provider
    def provide_database_config(
        self, db_host: str, db_port: int, db_name: str
    ) -> DatabaseConfig:
        """Provider parameters are auto-injected"""
        return DatabaseConfig(
            host=db_host,
            port=db_port,
            database=db_name
        )

    @provider
    def provide_database(self, config: DatabaseConfig) -> Database:
        """Compose dependencies with initialization logic"""
        db = Database(config)
        db.connect()  # Custom initialization
        return db

# Bind config values
container.bind_instance("db_host", "localhost")
container.bind_instance("db_port", 5432)
container.bind_instance("db_name", "myapp")

# Install module
container.install_module(AppModule())

# Get fully initialized database
db = container[Database]

The @provider pattern is the most powerful way to create complex dependencies.

Basic Provider

class ServiceModule(ProviderModule):
    @provider
    def provide_logger(self, app_name: str, log_level: str) -> Logger:
        """Return type annotation determines what this provides"""
        return Logger(name=app_name, level=log_level)

# Bind parameters
container.bind_instance("app_name", "MyApp")
container.bind_instance("log_level", "INFO")

# Install and use
container.install_module(ServiceModule())
logger = container[Logger]

Provider with Dependencies

class UserModule(ProviderModule):
    @provider
    def provide_user_service(
        self, database: Database, cache: Cache, logger: Logger
    ) -> UserService:
        """Dependencies are auto-injected"""
        return UserService(db=database, cache=cache, logger=logger)

Provider with Initialization

class DatabaseModule(ProviderModule):
    @provider
    def provide_database(self, config: DatabaseConfig) -> Database:
        """Perform complex initialization"""
        db = Database(config)
        db.connect()
        db.setup_tables()
        db.run_migrations()
        return db

Environment-Specific Providers

class EnvironmentModule(ProviderModule):
    def __init__(self, environment: str):
        self.environment = environment

    @provider
    def provide_database_config(self) -> DatabaseConfig:
        """Use module state for environment-specific logic"""
        if self.environment == "production":
            return DatabaseConfig(
                host="prod-db.example.com",
                port=5432,
                ssl=True
            )
        else:  # development
            return DatabaseConfig(
                host="localhost",
                port=5432,
                ssl=False
            )

# Usage
container.install_module(EnvironmentModule("production"))

Complete Example

from injectq import InjectQ
from injectq.modules import ProviderModule, provider

# Domain models
class DatabaseConfig:
    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port

class Database:
    def __init__(self, config: DatabaseConfig):
        self.config = config
        self.connected = False

    def connect(self):
        self.connected = True
        print(f"Connected to {self.config.host}")

class UserService:
    def __init__(self, db: Database, logger: Logger):
        self.db = db
        self.logger = logger

# Provider module
class ApplicationModule(ProviderModule):
    @provider
    def provide_database_config(
        self, db_host: str, db_port: int
    ) -> DatabaseConfig:
        return DatabaseConfig(host=db_host, port=db_port)

    @provider
    def provide_database(self, config: DatabaseConfig) -> Database:
        db = Database(config)
        db.connect()  # Initialize
        return db

    @provider
    def provide_logger(self, log_level: str) -> Logger:
        return Logger(level=log_level)

    @provider
    def provide_user_service(
        self, db: Database, logger: Logger
    ) -> UserService:
        return UserService(db=db, logger=logger)

# Setup
container = InjectQ.get_instance()

# Bind configuration
container.bind_instance("db_host", "localhost")
container.bind_instance("db_port", 5432)
container.bind_instance("log_level", "INFO")

# Install module
container.install_module(ApplicationModule())

# Use services
user_service = container[UserService]
# Database is connected, logger is configured

Module Patterns

Domain Modules

class UserModule(ProviderModule):
    @provider
    def provide_user_repository(self, db: Database) -> UserRepository:
        return UserRepository(db)

    @provider
    def provide_user_service(self, repo: UserRepository) -> UserService:
        return UserService(repo)

class OrderModule(ProviderModule):
    @provider
    def provide_order_repository(self, db: Database) -> OrderRepository:
        return OrderRepository(db)

    @provider
    def provide_order_service(self, repo: OrderRepository) -> OrderService:
        return OrderService(repo)

Infrastructure Module

class InfrastructureModule(ProviderModule):
    @provider
    def provide_database(self, db_url: str) -> Database:
        return Database(db_url)

    @provider
    def provide_cache(self, redis_url: str) -> Cache:
        return RedisCache(redis_url)

    @provider
    def provide_queue(self, queue_url: str) -> Queue:
        return MessageQueue(queue_url)

Testing Modules

class TestModule(ProviderModule):
    @provider
    def provide_database(self) -> Database:
        return InMemoryDatabase()

    @provider
    def provide_cache(self) -> Cache:
        return MockCache()

# Test setup
def test_user_service():
    container = InjectQ.get_instance()
    container.install_module(TestModule())

    service = container[UserService]
    # Uses mocked dependencies

Best Practices

✅ Use Providers for Complex Setup

@provider
def provide_database(self, config: DatabaseConfig) -> Database:
    db = Database(config)
    db.connect()
    db.run_migrations()
    return db

✅ One Module Per Domain

class UserModule(ProviderModule): pass
class OrderModule(ProviderModule): pass
class PaymentModule(ProviderModule): pass

✅ Environment-Specific Modules

class ProductionModule(ProviderModule):
    def __init__(self):
        self.environment = "production"

    @provider
    def provide_config(self) -> Config:
        return Config(ssl=True, debug=False)

✅ Type Annotations Required

@provider
def provide_database(self, config: DatabaseConfig) -> Database:
    """Return type annotation is required"""
    return Database(config)

❌ Avoid God Modules

# ❌ Bad - everything in one module
class EverythingModule(ProviderModule):
    # 50+ providers...

# ✅ Good - focused modules
class DatabaseModule(ProviderModule): pass
class CacheModule(ProviderModule): pass
class EmailModule(ProviderModule): pass

Summary

  • Simple Module: Fluent API for basic bindings
  • Configuration Module: Bind config dictionaries
  • Provider Module: Most powerful - complex initialization

Provider Pattern Benefits: - Auto-inject dependencies into provider methods - Custom initialization logic - Environment-specific configuration - Clean, organized code

Key Points: - Use @provider decorator on methods - Return type annotation determines what's provided - Parameters are auto-injected from container - Great for multi-step initialization

Next: FastAPI Integration | Testing