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?