Modules & Providers¶
Modules and providers organize your dependency injection configuration into reusable, composable units that make your application more maintainable and testable.
๐ฏ What are Modules?¶
Modules are containers for related bindings that group together services with similar responsibilities or from the same domain.
from injectq import Module, InjectQ
# Database module - groups all database-related services
class DatabaseModule(Module):
def configure(self, binder):
binder.bind(DatabaseConnection, PostgresConnection())
binder.bind(UserRepository, UserRepositoryImpl())
binder.bind(OrderRepository, OrderRepositoryImpl())
# Email module - groups email-related services
class EmailModule(Module):
def configure(self, binder):
binder.bind(EmailService, SmtpEmailService())
binder.bind(EmailTemplateEngine, JinjaTemplateEngine())
# Application setup
container = InjectQ()
container.install(DatabaseModule())
container.install(EmailModule())
# Services are now available
user_repo = container.get(UserRepository)
email_svc = container.get(EmailService)
๐๏ธ Why Use Modules?¶
โ Benefits¶
- Organization - Group related services together
- Reusability - Reuse modules across applications
- Testability - Easy to replace modules in tests
- Maintainability - Clear separation of concerns
- Composition - Combine modules for different environments
# Production configuration
container.install(DatabaseModule())
container.install(EmailModule())
container.install(CacheModule())
# Test configuration
container.install(InMemoryDatabaseModule())
container.install(MockEmailModule())
container.install(NoOpCacheModule())
โ Without Modules¶
# โ All bindings in one place - hard to maintain
container = InjectQ()
# Database bindings
container.bind(DatabaseConnection, PostgresConnection())
container.bind(UserRepository, UserRepositoryImpl())
# ... 20 more database bindings
# Email bindings
container.bind(EmailService, SmtpEmailService())
# ... 10 more email bindings
# Cache bindings
container.bind(CacheService, RedisCache())
# ... 5 more cache bindings
# Total: 35+ scattered bindings
๐ง Module Types¶
Configuration Module¶
Configuration modules bind interfaces to implementations and configure services.
from injectq import Module
class DatabaseModule(Module):
def __init__(self, connection_string: str):
self.connection_string = connection_string
def configure(self, binder):
# Bind interfaces to implementations
binder.bind(IDatabaseConnection, PostgresConnection(self.connection_string))
binder.bind(IUserRepository, UserRepository())
binder.bind(IOrderRepository, OrderRepository())
# Configure with specific settings
binder.bind(DatabaseConfig, DatabaseConfig(max_connections=20))
# Usage
container.install(DatabaseModule("postgresql://localhost/mydb"))
Provider Module¶
Provider modules use factory functions to create complex service instances.
from injectq import Module, provider
class ServiceModule(Module):
@provider
def create_database_pool(self) -> DatabasePool:
"""Factory for database connection pool"""
return DatabasePool(
host="localhost",
port=5432,
max_connections=20,
min_connections=5
)
@provider
def create_cache_service(self, pool: DatabasePool) -> ICache:
"""Factory for cache service with dependencies"""
return RedisCache(
host="redis-server",
db_pool=pool
)
# Usage
container.install(ServiceModule())
cache = container.get(ICache) # Gets RedisCache with pool
Conditional Module¶
Conditional modules configure services based on environment or conditions.
class EnvironmentModule(Module):
def __init__(self, environment: str):
self.environment = environment
def configure(self, binder):
if self.environment == "production":
binder.bind(IDatabase, PostgresDatabase())
binder.bind(ICache, RedisCache())
elif self.environment == "testing":
binder.bind(IDatabase, InMemoryDatabase())
binder.bind(ICache, NoOpCache())
else: # development
binder.bind(IDatabase, PostgresDatabase())
binder.bind(ICache, InMemoryCache())
# Usage
container.install(EnvironmentModule(os.getenv("ENV", "development")))
๐จ Module Patterns¶
Domain Module¶
Group services by business domain.
# User domain module
class UserModule(Module):
def configure(self, binder):
binder.bind(IUserRepository, UserRepository())
binder.bind(IUserService, UserService())
binder.bind(IUserValidator, UserValidator())
# Order domain module
class OrderModule(Module):
def configure(self, binder):
binder.bind(IOrderRepository, OrderRepository())
binder.bind(IOrderService, OrderService())
binder.bind(IOrderValidator, OrderValidator())
# Payment domain module
class PaymentModule(Module):
def configure(self, binder):
binder.bind(IPaymentProcessor, StripeProcessor())
binder.bind(IPaymentRepository, PaymentRepository())
# Application assembly
container.install(UserModule())
container.install(OrderModule())
container.install(PaymentModule())
Infrastructure Module¶
Group infrastructure services.
class InfrastructureModule(Module):
def configure(self, binder):
# Database
binder.bind(IDatabase, PostgresDatabase())
# Cache
binder.bind(ICache, RedisCache())
# Message queue
binder.bind(IMessageQueue, RabbitMQ())
# External APIs
binder.bind(IPaymentAPI, StripeAPI())
binder.bind(IEmailAPI, SendGridAPI())
Cross-Cutting Module¶
Group cross-cutting concerns.
class CrossCuttingModule(Module):
def configure(self, binder):
# Logging
binder.bind(ILogger, StructuredLogger())
# Metrics
binder.bind(IMetrics, PrometheusMetrics())
# Security
binder.bind(IAuthenticator, JWTAuthenticator())
binder.bind(IAuthorizer, RBACAuthorizer())
# Validation
binder.bind(IValidator, FluentValidator())
๐ Module Composition¶
Module Dependencies¶
Modules can depend on services from other modules.
class EmailModule(Module):
def configure(self, binder):
binder.bind(IEmailService, SmtpEmailService())
binder.bind(IEmailTemplate, JinjaTemplate())
class NotificationModule(Module):
def configure(self, binder):
# Depends on EmailModule's IEmailService
binder.bind(INotificationService, EmailNotificationService())
# Installation order matters
container.install(EmailModule()) # First
container.install(NotificationModule()) # Second
Module Overrides¶
Override bindings for testing or different environments.
class ProductionModule(Module):
def configure(self, binder):
binder.bind(IDatabase, PostgresDatabase())
class TestModule(Module):
def configure(self, binder):
# Override production database
binder.bind(IDatabase, InMemoryDatabase())
# Test setup
container.install(ProductionModule())
container.install(TestModule()) # Overrides database binding
Module Inheritance¶
Extend modules for specialization.
class BaseDatabaseModule(Module):
def configure(self, binder):
binder.bind(IDatabaseConnection, self.create_connection())
def create_connection(self):
raise NotImplementedError
class PostgresModule(BaseDatabaseModule):
def create_connection(self):
return PostgresConnection("postgresql://...")
class MySQLModule(BaseDatabaseModule):
def create_connection(self):
return MySQLConnection("mysql://...")
๐งช Testing with Modules¶
Module Replacement¶
Replace entire modules for testing.
class MockEmailModule(Module):
def configure(self, binder):
binder.bind(IEmailService, MockEmailService())
# Test setup
def test_user_registration():
container = InjectQ()
# Use real modules
container.install(DatabaseModule())
container.install(UserModule())
# Replace email module with mock
container.install(MockEmailModule())
# Test
service = container.get(IUserService)
service.register_user("test@example.com", "password")
# Verify email was "sent"
mock_email = container.get(IEmailService)
assert len(mock_email.sent_emails) == 1
Partial Overrides¶
Override only specific bindings.
class TestOverridesModule(Module):
def configure(self, binder):
# Only override the repository, keep other services
binder.bind(IUserRepository, MockUserRepository())
# Test with partial override
container.install(ProductionModule()) # All production services
container.install(TestOverridesModule()) # Override just repository
Test Module Composition¶
def create_test_container():
"""Factory for test containers"""
container = InjectQ()
# Install test versions of all modules
container.install(TestDatabaseModule())
container.install(TestEmailModule())
container.install(TestCacheModule())
return container
def test_complete_workflow():
container = create_test_container()
# Test entire workflow with mocked dependencies
workflow = container.get(OrderWorkflow)
result = workflow.process_order(order_data)
assert result.success
๐จ Module Best Practices¶
1. Single Responsibility¶
# โ
Good: Single responsibility
class DatabaseModule(Module):
"""Handles all database-related bindings"""
class EmailModule(Module):
"""Handles all email-related bindings"""
# โ Bad: Multiple responsibilities
class UtilsModule(Module):
"""Handles database, email, cache, logging... everything!"""
2. Interface-Based Binding¶
# โ
Good: Bind to interfaces
class RepositoryModule(Module):
def configure(self, binder):
binder.bind(IUserRepository, SqlUserRepository())
binder.bind(IOrderRepository, SqlOrderRepository())
# โ Bad: Bind to implementations
class RepositoryModule(Module):
def configure(self, binder):
binder.bind(SqlUserRepository, SqlUserRepository())
binder.bind(SqlOrderRepository, SqlOrderRepository())
3. Configuration Through Parameters¶
# โ
Good: Configurable modules
class DatabaseModule(Module):
def __init__(self, config: DatabaseConfig):
self.config = config
def configure(self, binder):
binder.bind(IDatabase, PostgresDatabase(self.config))
# โ Bad: Hard-coded configuration
class DatabaseModule(Module):
def configure(self, binder):
binder.bind(IDatabase, PostgresDatabase("hardcoded-connection"))
4. Clear Naming Conventions¶
# โ
Good naming
class UserDomainModule(Module): pass
class InfrastructureModule(Module): pass
class TestOverridesModule(Module): pass
# โ Bad naming
class Module1(Module): pass
class MyModule(Module): pass
class StuffModule(Module): pass
5. Documentation¶
class PaymentProcessingModule(Module):
"""
Payment Processing Module
Provides services for processing payments through various
payment gateways. Supports Stripe, PayPal, and bank transfers.
Bindings:
- IPaymentProcessor -> StripeProcessor (primary)
- IPaymentRepository -> DatabasePaymentRepository
- IPaymentValidator -> PaymentValidator
Dependencies:
- Requires InfrastructureModule for database access
- Requires SecurityModule for encryption
Environment Variables:
- STRIPE_API_KEY: Stripe API key
- PAYPAL_CLIENT_ID: PayPal client ID
"""
pass
โก Advanced Module Features¶
Dynamic Module Loading¶
def load_modules_from_config(config_file: str) -> List[Module]:
"""Load modules based on configuration"""
config = load_config(config_file)
modules = []
if config.get("database.enabled"):
modules.append(DatabaseModule(config["database"]))
if config.get("email.enabled"):
modules.append(EmailModule(config["email"]))
if config.get("cache.enabled"):
modules.append(CacheModule(config["cache"]))
return modules
# Usage
modules = load_modules_from_config("app_config.yaml")
for module in modules:
container.install(module)
Module Health Checks¶
class HealthCheckModule(Module):
def configure(self, binder):
binder.bind(IHealthChecker, CompositeHealthChecker())
@provider
def create_health_checker(self) -> IHealthChecker:
checkers = [
DatabaseHealthChecker(),
CacheHealthChecker(),
EmailHealthChecker(),
]
return CompositeHealthChecker(checkers)
# Usage
health_checker = container.get(IHealthChecker)
status = health_checker.check_all()
Module Metrics¶
class MetricsModule(Module):
def configure(self, binder):
binder.bind(IMetrics, PrometheusMetrics())
@provider
def create_metrics(self) -> IMetrics:
metrics = PrometheusMetrics()
# Add module-specific metrics
metrics.gauge("modules_loaded", len(container._modules))
metrics.counter("bindings_created", len(container._bindings))
return metrics
๐ฏ Summary¶
Modules provide:
- Organization - Group related bindings together
- Reusability - Reuse across applications and tests
- Testability - Easy replacement for testing
- Maintainability - Clear separation of concerns
- Composition - Flexible combination of modules
Key patterns: - Domain modules for business logic - Infrastructure modules for technical services - Provider modules for factory functions - Conditional modules for environment-specific config
Best practices: - Single responsibility per module - Interface-based bindings - Configurable through parameters - Clear naming conventions - Comprehensive documentation
Ready to explore framework integrations?