Provider Modules - InjectQ Documentation
Author: InjectQ Contributors
Description: Modern Python dependency injection library

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?