Skip to content

Provider Modules

Provider modules use factory functions and the @provider decorator to create complex service instances with dependency injection support.

๐ŸŽฏ What are Providers?

Providers are factory functions that create service instances, automatically receiving their dependencies through injection.

from injectq import Module, provider, InjectQ

class ServiceModule(Module):
    @provider
    def create_database_pool(self) -> DatabasePool:
        """Factory for database connection pool"""
        return DatabasePool(
            host="localhost",
            port=5432,
            max_connections=20
        )

    @provider
    def create_user_service(self, user_repo: IUserRepository, email_svc: IEmailService) -> IUserService:
        """Factory for user service with dependencies"""
        return UserService(user_repo, email_svc)

# Usage
container = InjectQ()
container.install(ServiceModule())

# Services are created with dependencies injected
user_service = container.get(IUserService)  # Gets UserService with injected dependencies

๐Ÿ”ง Creating Provider Methods

Basic Provider

from injectq import Module, provider

class DatabaseModule(Module):
    @provider
    def database_connection(self) -> IDatabaseConnection:
        """Create database connection"""
        return PostgresConnection(
            host="localhost",
            database="myapp"
        )

Provider with Dependencies

class ServiceModule(Module):
    @provider
    def user_repository(self, db: IDatabaseConnection) -> IUserRepository:
        """Create user repository with database dependency"""
        return SqlUserRepository(db)

    @provider
    def user_service(self, user_repo: IUserRepository, email_svc: IEmailService) -> IUserService:
        """Create user service with its dependencies"""
        return UserService(user_repo, email_svc)

Provider with Configuration

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

    @provider
    def database_pool(self) -> IDatabasePool:
        """Create database pool with configuration"""
        return DatabasePool(
            host=self.config.database_host,
            port=self.config.database_port,
            max_connections=self.config.max_connections
        )

    @provider
    def cache_service(self) -> ICache:
        """Create cache service with configuration"""
        if self.config.use_redis:
            return RedisCache(self.config.redis_url)
        else:
            return InMemoryCache()

๐ŸŽจ Provider Patterns

Complex Object Creation

class InfrastructureModule(Module):
    @provider
    def message_queue(self) -> IMessageQueue:
        """Create message queue with retry logic"""
        queue = RabbitMQConnection(
            host="rabbitmq-server",
            port=5672,
            credentials=self._load_credentials()
        )

        # Configure retry policy
        queue.retry_policy = ExponentialBackoffRetry(
            max_attempts=5,
            base_delay=1.0
        )

        return queue

    @provider
    def payment_processor(self, mq: IMessageQueue, db: IDatabase) -> IPaymentProcessor:
        """Create payment processor with dependencies"""
        processor = StripePaymentProcessor(
            api_key=os.getenv("STRIPE_API_KEY"),
            message_queue=mq,
            database=db
        )

        # Configure webhooks
        processor.webhook_secret = os.getenv("STRIPE_WEBHOOK_SECRET")

        return processor

    def _load_credentials(self) -> Credentials:
        """Load MQ credentials from secure storage"""
        return Credentials(
            username=os.getenv("MQ_USER"),
            password=os.getenv("MQ_PASS")
        )

Conditional Provider

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

    @provider
    def email_service(self) -> IEmailService:
        """Create email service based on environment"""
        if self.environment == "production":
            return SmtpEmailService(
                host="smtp.gmail.com",
                port=587,
                credentials=self._load_smtp_credentials()
            )
        elif self.environment == "testing":
            return MockEmailService()
        else:
            return ConsoleEmailService()  # Development

    @provider
    def cache_service(self) -> ICache:
        """Create cache service based on environment"""
        if self.environment == "production":
            return RedisCache(host="redis-cluster")
        else:
            return InMemoryCache()

Resource Management Provider

class ResourceModule(Module):
    @provider
    def database_connection_pool(self) -> IDatabasePool:
        """Create managed database connection pool"""
        pool = DatabasePool(
            host="localhost",
            max_connections=20,
            min_connections=5
        )

        # Register cleanup
        import atexit
        atexit.register(pool.close_all)

        return pool

    @provider
    def file_manager(self) -> IFileManager:
        """Create file manager with temp directory"""
        temp_dir = tempfile.mkdtemp(prefix="app_")

        manager = FileManager(temp_dir)

        # Register cleanup
        import atexit
        atexit.register(lambda: shutil.rmtree(temp_dir))

        return manager

๐Ÿ”„ Provider Dependencies

Multi-Level Dependencies

class ApplicationModule(Module):
    @provider
    def database_connection(self) -> IDatabaseConnection:
        """Level 1: Basic connection"""
        return PostgresConnection("postgresql://...")

    @provider
    def user_repository(self, db: IDatabaseConnection) -> IUserRepository:
        """Level 2: Depends on connection"""
        return SqlUserRepository(db)

    @provider
    def order_repository(self, db: IDatabaseConnection) -> IOrderRepository:
        """Level 2: Depends on connection"""
        return SqlOrderRepository(db)

    @provider
    def user_service(self, user_repo: IUserRepository, email_svc: IEmailService) -> IUserService:
        """Level 3: Depends on repository and email"""
        return UserService(user_repo, email_svc)

    @provider
    def order_service(self, order_repo: IOrderRepository, payment_svc: IPaymentService) -> IOrderService:
        """Level 3: Depends on repository and payment"""
        return OrderService(order_repo, payment_svc)

Circular Dependency Prevention

# โœ… Good: No circular dependencies
class GoodModule(Module):
    @provider
    def service_a(self, repo: IRepository) -> IServiceA:
        return ServiceA(repo)

    @provider
    def service_b(self, service_a: IServiceA) -> IServiceB:
        return ServiceB(service_a)

# โŒ Bad: Circular dependency
class BadModule(Module):
    @provider
    def service_a(self, service_b: IServiceB) -> IServiceA:
        return ServiceA(service_b)  # Depends on B

    @provider
    def service_b(self, service_a: IServiceA) -> IServiceB:
        return ServiceB(service_a)  # Depends on A

Optional Dependencies

class FlexibleModule(Module):
    @provider
    def notification_service(self, email_svc: Optional[IEmailService] = None) -> INotificationService:
        """Create notification service with optional email"""
        if email_svc:
            return EmailNotificationService(email_svc)
        else:
            return ConsoleNotificationService()

    @provider
    def cache_service(self) -> ICache:
        """Create cache service with fallback"""
        try:
            return RedisCache(host="redis-server")
        except ConnectionError:
            return InMemoryCache()

๐Ÿงช Testing with Providers

Provider Testing

def test_provider_creation():
    """Test that providers create correct instances"""
    container = InjectQ()
    container.install(ServiceModule())

    # Test provider-created service
    user_service = container.get(IUserService)
    assert isinstance(user_service, UserService)

    # Test dependencies were injected
    assert user_service.user_repository is not None
    assert user_service.email_service is not None

def test_provider_with_mocks():
    """Test provider with mocked dependencies"""
    container = InjectQ()

    # Mock dependencies
    mock_repo = MockUserRepository()
    mock_email = MockEmailService()

    container.bind(IUserRepository, mock_repo)
    container.bind(IEmailService, mock_email)

    # Install module with providers
    container.install(ServiceModule())

    # Get provider-created service
    user_service = container.get(IUserService)

    # Verify mocks were used
    assert user_service.user_repository is mock_repo
    assert user_service.email_service is mock_email

Provider Override

class TestProvidersModule(Module):
    @provider
    def user_service(self) -> IUserService:
        """Override provider for testing"""
        return MockUserService()

def test_with_provider_override():
    """Test with overridden provider"""
    container = InjectQ()

    # Install production module
    container.install(ServiceModule())

    # Override specific provider
    container.install(TestProvidersModule())

    # Get service
    user_service = container.get(IUserService)

    # Should be mock, not real service
    assert isinstance(user_service, MockUserService)

Provider Dependency Testing

def test_provider_dependencies():
    """Test that provider dependencies are correctly resolved"""
    container = InjectQ()
    container.install(ComplexModule())

    # Get service with complex dependency chain
    payment_processor = container.get(IPaymentProcessor)

    # Verify entire dependency chain
    assert payment_processor.message_queue is not None
    assert payment_processor.database is not None

    # Verify MQ has its dependencies
    mq = payment_processor.message_queue
    assert mq.credentials is not None
    assert mq.retry_policy is not None

๐Ÿšจ Provider Anti-Patterns

1. Complex Logic in Providers

# โŒ Bad: Too much logic in provider
class BadModule(Module):
    @provider
    def complex_service(self) -> IService:
        # Too much setup logic
        config = self._load_config()
        credentials = self._decrypt_credentials(config)
        connection = self._create_connection(credentials)
        pool = self._create_pool(connection)
        service = self._create_service(pool)

        # Business logic mixed in
        if config.environment == "prod":
            service.enable_monitoring()
        else:
            service.disable_monitoring()

        return service

# โœ… Good: Extract logic to separate methods/classes
class GoodModule(Module):
    def __init__(self, config: AppConfig):
        self.config = config

    @provider
    def service(self) -> IService:
        """Simple provider using factory"""
        return ServiceFactory.create(self.config)

class ServiceFactory:
    @staticmethod
    def create(config: AppConfig) -> IService:
        credentials = CredentialLoader.load(config)
        connection = ConnectionFactory.create(credentials)
        pool = PoolFactory.create(connection, config)
        service = ServiceFactory._create_service(pool, config)

        if config.environment == "prod":
            service.enable_monitoring()

        return service

2. Provider Side Effects

# โŒ Bad: Side effects in provider
class BadModule(Module):
    @provider
    def database_service(self) -> IDatabaseService:
        service = DatabaseService()

        # Side effect: modifies global state
        global_config.database_initialized = True

        # Side effect: creates files
        os.makedirs("/tmp/app_data", exist_ok=True)

        return service

# โœ… Good: Pure providers
class GoodModule(Module):
    @provider
    def database_service(self) -> IDatabaseService:
        return DatabaseService()

    def initialize(self):
        """Call this separately for side effects"""
        global_config.database_initialized = True
        os.makedirs("/tmp/app_data", exist_ok=True)

3. Provider Tight Coupling

# โŒ Bad: Tight coupling in provider
class BadModule(Module):
    @provider
    def user_service(self) -> IUserService:
        # Direct instantiation
        repo = SqlUserRepository(PostgresConnection())
        email = SmtpEmailService()
        return UserService(repo, email)

# โœ… Good: Loose coupling through dependencies
class GoodModule(Module):
    @provider
    def user_service(self, user_repo: IUserRepository, email_svc: IEmailService) -> IUserService:
        return UserService(user_repo, email_svc)

    @provider
    def user_repository(self, db: IDatabaseConnection) -> IUserRepository:
        return SqlUserRepository(db)

    @provider
    def email_service(self) -> IEmailService:
        return SmtpEmailService()

4. Provider Overuse

# โŒ Bad: Provider for everything
class OveruseModule(Module):
    @provider
    def simple_string(self) -> str:
        return "hello"

    @provider
    def simple_number(self) -> int:
        return 42

    @provider
    def simple_list(self) -> List[str]:
        return ["a", "b", "c"]

# โœ… Good: Use providers for complex objects only
class GoodModule(Module):
    @provider
    def complex_service(self, repo: IRepository, config: AppConfig) -> IService:
        return ComplexService(repo, config)

    def configure(self, binder):
        # Simple values can use regular bindings
        binder.bind(str, "hello")
        binder.bind(int, 42)
        binder.bind(List[str], ["a", "b", "c"])

๐Ÿ† Best Practices

1. Keep Providers Simple

# โœ… Simple provider
class SimpleModule(Module):
    @provider
    def database_pool(self) -> IDatabasePool:
        return DatabasePool(host="localhost", max_conn=20)

# โœ… Extract complex logic
class ComplexModule(Module):
    @provider
    def payment_processor(self) -> IPaymentProcessor:
        return PaymentProcessorFactory.create(self.config)

2. Use Meaningful Names

# โœ… Good naming
class GoodModule(Module):
    @provider
    def user_notification_service(self) -> IUserNotificationService:
        return EmailUserNotificationService()

    @provider
    def admin_notification_service(self) -> IAdminNotificationService:
        return SmsAdminNotificationService()

# โŒ Bad naming
class BadModule(Module):
    @provider
    def service1(self) -> IService1:
        return Service1Impl()

    @provider
    def svc2(self) -> IService2:
        return Service2Impl()

3. Document Providers

class DocumentedModule(Module):
    @provider
    def user_authentication_service(self, user_repo: IUserRepository, jwt_config: JWTConfig) -> IAuthenticationService:
        """
        Create user authentication service.

        This provider creates an authentication service that handles
        user login, logout, and token validation.

        Args:
            user_repo: Repository for user data access
            jwt_config: Configuration for JWT token handling

        Returns:
            Configured authentication service instance

        Dependencies:
            - IUserRepository: For user data access
            - JWTConfig: For token configuration

        Notes:
            - Uses bcrypt for password hashing
            - Tokens expire after 24 hours
            - Supports refresh token rotation
        """
        return JWTAuthenticationService(user_repo, jwt_config)

4. Handle Errors Gracefully

class RobustModule(Module):
    @provider
    def external_api_client(self) -> IExternalAPI:
        """Create external API client with error handling"""
        try:
            return HttpExternalAPI(
                base_url=os.getenv("API_BASE_URL"),
                api_key=os.getenv("API_KEY"),
                timeout=30
            )
        except (ValueError, ConnectionError) as e:
            # Fallback to mock in case of configuration errors
            logger.warning(f"Failed to create external API client: {e}")
            return MockExternalAPI()

    @provider
    def cache_service(self) -> ICache:
        """Create cache service with fallback"""
        cache_configs = [
            lambda: RedisCache(host=os.getenv("REDIS_HOST")),
            lambda: MemcachedCache(host=os.getenv("MEMCACHED_HOST")),
            lambda: InMemoryCache(),  # Always works
        ]

        for config_func in cache_configs:
            try:
                return config_func()
            except Exception as e:
                logger.warning(f"Failed to create cache: {e}")
                continue

        raise RuntimeError("All cache configurations failed")

5. Test Providers Thoroughly

def test_provider_error_handling():
    """Test provider error handling"""
    # Test with missing environment variables
    with patch.dict(os.environ, {}, clear=True):
        container = InjectQ()
        container.install(RobustModule())

        # Should get fallback mock
        api_client = container.get(IExternalAPI)
        assert isinstance(api_client, MockExternalAPI)

def test_provider_fallback_chain():
    """Test provider fallback chain"""
    container = InjectQ()
    container.install(RobustModule())

    # Should try Redis first
    cache = container.get(ICache)
    # Verify it's the expected type based on configuration

โšก Advanced Provider Features

Async Providers

class AsyncModule(Module):
    @provider
    async def async_database_pool(self) -> IAsyncDatabasePool:
        """Create async database pool"""
        pool = await AsyncDatabasePool.create(
            host="localhost",
            port=5432,
            database="myapp"
        )
        return pool

    @provider
    async def async_user_service(self, pool: IAsyncDatabasePool) -> IAsyncUserService:
        """Create async user service"""
        return AsyncUserService(pool)

Provider Scopes

class ScopedProvidersModule(Module):
    @provider(scope="singleton")
    def application_config(self) -> IAppConfig:
        """Singleton provider"""
        return AppConfig.from_env()

    @provider(scope="scoped")
    def request_context(self) -> IRequestContext:
        """Scoped provider"""
        return RequestContext()

    @provider(scope="transient")
    def validator(self) -> IValidator:
        """Transient provider"""
        return DataValidator()

Provider with Lifecycle

class LifecycleModule(Module):
    @provider
    def managed_service(self) -> IManagedService:
        """Create service with lifecycle management"""
        service = ManagedService()

        # Register lifecycle hooks
        container.on_shutdown(service.cleanup)

        return service

    @provider
    def health_check_service(self) -> IHealthChecker:
        """Create health checker for all providers"""
        return CompositeHealthChecker([
            DatabaseHealthCheck(),
            CacheHealthCheck(),
            ExternalAPIHealthCheck(),
        ])

๐ŸŽฏ Summary

Provider modules provide:

  • Factory functions - Create complex service instances
  • Dependency injection - Automatic dependency resolution
  • Flexibility - Handle complex creation logic
  • Testability - Easy to mock and override
  • Clean separation - Separate creation from usage

Key principles: - Keep providers simple and focused - Use meaningful names and documentation - Handle errors gracefully with fallbacks - Test thoroughly including error cases - Avoid side effects and tight coupling

Common patterns: - Complex object creation with dependencies - Conditional providers based on environment - Resource management with cleanup - Multi-level dependency chains - Error handling with fallbacks

Ready to explore module composition?