Module Best Practices¶
Module best practices guide you to create maintainable, testable, and reusable modules that work well together in complex applications.
🎯 Module Design Principles¶
Single Responsibility Principle¶
Each module should have one clear, focused responsibility.
# ✅ Good: Single responsibility modules
class UserManagementModule(Module):
"""Handles user registration, authentication, and profiles"""
class OrderProcessingModule(Module):
"""Handles order creation, payment, and fulfillment"""
class EmailCommunicationModule(Module):
"""Handles all email sending and templates"""
class DatabaseInfrastructureModule(Module):
"""Handles database connections and migrations"""
# ❌ Bad: Multiple responsibilities
class EverythingModule(Module):
"""Handles users, orders, email, database, cache, logging..."""
Interface Segregation¶
Bind to interfaces, not implementations.
# ✅ Good: Interface-based bindings
class RepositoryModule(Module):
def configure(self, binder):
binder.bind(IUserRepository, SqlUserRepository())
binder.bind(IOrderRepository, SqlOrderRepository())
# ❌ Bad: Implementation bindings
class RepositoryModule(Module):
def configure(self, binder):
binder.bind(SqlUserRepository, SqlUserRepository())
binder.bind(SqlOrderRepository, SqlOrderRepository())
Dependency Inversion¶
Depend on abstractions, not concretions.
# ✅ Good: Depends on interfaces
class ServiceModule(Module):
@provider
def user_service(self, user_repo: IUserRepository, email_svc: IEmailService) -> IUserService:
return UserService(user_repo, email_svc)
# ❌ Bad: Depends on implementations
class ServiceModule(Module):
@provider
def user_service(self) -> IUserService:
return UserService(SqlUserRepository(), SmtpEmailService())
🏗️ Module Structure Guidelines¶
Consistent Module Structure¶
class WellStructuredModule(Module):
"""
Module docstring describing responsibility and dependencies.
"""
def __init__(self, config: ModuleConfig):
"""Initialize with configuration."""
self.config = config
def configure(self, binder):
"""
Configure bindings for this module.
This method should:
1. Bind interfaces to implementations
2. Configure services with settings
3. Set up any required infrastructure
"""
# Interface bindings
binder.bind(IMyService, MyServiceImpl(self.config))
# Configuration bindings
binder.bind(ModuleConfig, self.config)
# Optional: Provider methods for complex services
@provider
def complex_service(self, dep1: IDep1, dep2: IDep2) -> IComplexService:
"""Provider for complex service creation."""
return ComplexService(dep1, dep2, self.config)
Configuration Management¶
@dataclass
class DatabaseConfig:
"""Configuration for database module."""
host: str
port: int
database: str
username: str
password: str
max_connections: int = 20
class DatabaseModule(Module):
"""Database infrastructure module."""
def __init__(self, config: DatabaseConfig):
self.config = config
def configure(self, binder):
# Bind configuration
binder.bind(DatabaseConfig, self.config)
# Bind services using configuration
binder.bind(IDatabaseConnection, PostgresConnection(self.config))
@provider
def connection_pool(self) -> IDatabasePool:
"""Create database connection pool."""
return DatabasePool(
host=self.config.host,
port=self.config.port,
database=self.config.database,
username=self.config.username,
password=self.config.password,
max_connections=self.config.max_connections
)
🔧 Naming Conventions¶
Module Naming¶
# ✅ Good naming patterns
class UserManagementModule(Module): pass # Feature + Module
class DatabaseInfrastructureModule(Module): pass # Layer + Feature + Module
class EmailNotificationModule(Module): pass # Technology + Feature + Module
class PaymentProcessingModule(Module): pass # Domain + Feature + Module
# ❌ Bad naming
class Module1(Module): pass # Too generic
class MyModule(Module): pass # Not descriptive
class UserStuffModule(Module): pass # Vague
Provider Method Naming¶
class ServiceModule(Module):
# ✅ Good: Descriptive names
@provider
def user_registration_service(self) -> IUserRegistrationService:
return UserRegistrationService()
@provider
def email_notification_service(self) -> IEmailNotificationService:
return EmailNotificationService()
# ❌ Bad: Generic names
@provider
def service1(self) -> IService1:
return Service1Impl()
@provider
def create_service(self) -> IService:
return Service()
Interface Naming¶
# ✅ Good: Clear interface names
class IUserRepository: pass
class IEmailService: pass
class IPaymentProcessor: pass
# ❌ Bad: Unclear names
class IRepo: pass
class IService: pass
class IProcessor: pass
📚 Documentation Standards¶
Module Documentation¶
class UserManagementModule(Module):
"""
User Management Module
Provides comprehensive user management functionality including:
- User registration and authentication
- Profile management
- Password reset functionality
- User role and permission management
Bindings Provided:
- IUserRepository -> SqlUserRepository
- IUserService -> UserService
- IAuthenticationService -> JWTAuthenticationService
- IPasswordResetService -> EmailPasswordResetService
Dependencies Required:
- DatabaseInfrastructureModule: For data persistence
- EmailCommunicationModule: For notifications
- SecurityModule: For authentication
Configuration:
- Requires UserConfig with JWT settings
- Database connection from infrastructure module
Installation Order:
Must be installed after DatabaseInfrastructureModule,
EmailCommunicationModule, and SecurityModule.
Environment Variables:
- JWT_SECRET: Secret key for JWT tokens
- PASSWORD_RESET_URL: Base URL for password reset links
Example:
config = UserConfig(jwt_secret="secret", reset_url="https://app.com/reset")
container.install(UserManagementModule(config))
"""
def __init__(self, config: UserConfig):
self.config = config
def configure(self, binder):
# Implementation...
pass
Provider Documentation¶
class ComplexServiceModule(Module):
@provider
def payment_processing_service(
self,
payment_repo: IPaymentRepository,
fraud_detector: IFraudDetector,
notification_svc: INotificationService,
config: PaymentConfig
) -> IPaymentProcessingService:
"""
Create payment processing service with all dependencies.
This provider creates a comprehensive payment processing service
that handles payment authorization, capture, refunds, and fraud
detection with real-time notifications.
Args:
payment_repo: Repository for payment data persistence
fraud_detector: Service for fraud detection and prevention
notification_svc: Service for sending payment notifications
config: Configuration for payment processing settings
Returns:
Fully configured payment processing service
Dependencies:
- IPaymentRepository: For payment data storage
- IFraudDetector: For fraud detection
- INotificationService: For payment notifications
- PaymentConfig: For payment settings
Notes:
- Supports multiple payment methods (credit card, PayPal, etc.)
- Includes fraud detection with configurable risk thresholds
- Sends real-time notifications for payment events
- Handles automatic retries for failed payments
Raises:
ConfigurationError: If payment configuration is invalid
DependencyError: If required dependencies are not available
"""
return PaymentProcessingService(
payment_repo,
fraud_detector,
notification_svc,
config
)
🧪 Testing Best Practices¶
Module Testing¶
def test_module_bindings():
"""Test that module provides expected bindings."""
container = InjectQ()
container.install(TestModule())
# Test all expected services are bound
service1 = container.get(IService1)
service2 = container.get(IService2)
config = container.get(ModuleConfig)
assert isinstance(service1, Service1Impl)
assert isinstance(service2, Service2Impl)
assert config.setting == "test_value"
def test_module_dependencies():
"""Test module with its dependencies."""
container = InjectQ()
# Install dependencies first
container.install(MockDependencyModule())
# Install module under test
container.install(ModuleUnderTest())
# Test integration
service = container.get(IService)
result = service.do_work()
assert result.success
Provider Testing¶
def test_provider_creation():
"""Test that providers create services correctly."""
container = InjectQ()
container.install(ServiceModule())
# Mock dependencies
mock_repo = MockRepository()
mock_email = MockEmailService()
container.bind(IRepository, mock_repo)
container.bind(IEmailService, mock_email)
# Get provider-created service
service = container.get(IService)
# Verify dependencies were injected
assert service.repository is mock_repo
assert service.email_service is mock_email
def test_provider_error_handling():
"""Test provider error handling."""
container = InjectQ()
container.install(ServiceModule())
# Test with missing dependency
with pytest.raises(DependencyResolutionError):
container.get(IService) # Should fail if dependencies not bound
Integration Testing¶
def test_module_integration():
"""Test multiple modules working together."""
container = create_integration_container()
# Test complete workflow across modules
user_service = container.get(IUserService)
order_service = container.get(IOrderService)
email_service = container.get(IEmailService)
# Create user
user = user_service.create_user("test@example.com", "password")
assert user.email == "test@example.com"
# Create order
order = order_service.create_order(user.id, [order_item])
assert order.user_id == user.id
# Verify notifications sent
assert len(email_service.sent_emails) == 2 # Welcome + order confirmation
def create_integration_container() -> InjectQ:
"""Create container for integration testing."""
container = InjectQ()
# Install test versions of all modules
container.install(TestDatabaseModule())
container.install(TestCacheModule())
container.install(UserManagementModule())
container.install(OrderProcessingModule())
container.install(MockEmailModule())
return container
🚨 Common Anti-Patterns¶
1. God Module¶
# ❌ Anti-pattern: God module
class GodModule(Module):
def configure(self, binder):
# Binds everything: database, cache, services, infrastructure...
binder.bind(IDatabase, PostgresDatabase())
binder.bind(ICache, RedisCache())
binder.bind(IUserService, UserService())
binder.bind(IOrderService, OrderService())
binder.bind(IEmailService, SmtpEmailService())
# ... 50+ more bindings
# ✅ Solution: Split into focused modules
class DatabaseModule(Module): pass
class CacheModule(Module): pass
class UserModule(Module): pass
class OrderModule(Module): pass
class EmailModule(Module): pass
2. Configuration Scattering¶
# ❌ Anti-pattern: Configuration scattered
class DatabaseModule(Module):
def configure(self, binder):
binder.bind(IDatabase, PostgresDatabase("hardcoded-url"))
class CacheModule(Module):
def configure(self, binder):
binder.bind(ICache, RedisCache("hardcoded-url"))
# ✅ Solution: Centralized configuration
@dataclass
class AppConfig:
database_url: str
redis_url: str
class ConfigModule(Module):
def __init__(self, config: AppConfig):
self.config = config
def configure(self, binder):
binder.bind(AppConfig, self.config)
class DatabaseModule(Module):
def configure(self, binder):
config = binder.get(AppConfig)
binder.bind(IDatabase, PostgresDatabase(config.database_url))
3. Tight Coupling¶
# ❌ Anti-pattern: Tight coupling
class TightlyCoupledModule(Module):
def configure(self, binder):
# Direct instantiation creates coupling
binder.bind(IService, Service(SqlRepository(), SmtpEmailService()))
# ✅ Solution: Loose coupling through interfaces
class LooselyCoupledModule(Module):
def configure(self, binder):
binder.bind(IService, Service()) # Dependencies resolved at runtime
4. Side Effects in Configure¶
# ❌ Anti-pattern: Side effects
class SideEffectModule(Module):
def configure(self, binder):
# Side effects in configuration
os.makedirs("/tmp/app_data", exist_ok=True)
self.initialize_database()
binder.bind(IService, Service())
# ✅ Solution: Pure configuration
class PureModule(Module):
def configure(self, binder):
binder.bind(IService, Service())
def initialize(self):
"""Call separately for side effects."""
os.makedirs("/tmp/app_data", exist_ok=True)
self.initialize_database()
5. Circular Dependencies¶
# ❌ Anti-pattern: Circular dependencies
class ModuleA(Module):
def configure(self, binder):
binder.bind(IServiceA, ServiceA()) # Depends on IServiceB
class ModuleB(Module):
def configure(self, binder):
binder.bind(IServiceB, ServiceB()) # Depends on IServiceA
# ✅ Solution: Break the cycle
class RefactoredModuleA(Module):
def configure(self, binder):
binder.bind(IServiceA, ServiceA(binder.get(IServiceB)))
class RefactoredModuleB(Module):
def configure(self, binder):
binder.bind(IServiceB, ServiceB())
# Install B first, then A
container.install(RefactoredModuleB())
container.install(RefactoredModuleA())
⚡ Advanced Patterns¶
Module Versioning¶
class VersionedModule(Module):
"""Module with version information and compatibility checking."""
VERSION = "2.1.0"
MIN_CONTAINER_VERSION = "1.5.0"
def __init__(self, config: ModuleConfig):
self.config = config
def configure(self, binder):
# Bind version information
binder.bind(ModuleVersion, self.VERSION)
# Bind services
binder.bind(IModuleService, ModuleService(self.VERSION))
@classmethod
def is_compatible(cls, container_version: str) -> bool:
"""Check compatibility with container version."""
from packaging import version
return version.parse(container_version) >= version.parse(cls.MIN_CONTAINER_VERSION)
Module Health Checks¶
class HealthCheckModule(Module):
"""Module that provides health checking for all services."""
def configure(self, binder):
binder.bind(IHealthChecker, ModuleHealthChecker())
@provider
def create_health_checker(self) -> IHealthChecker:
"""Create comprehensive health checker."""
return CompositeHealthChecker([
DatabaseHealthCheck(),
CacheHealthCheck(),
ExternalAPIHealthCheck(),
ServiceHealthCheck(),
])
class ServiceHealthCheck(HealthCheck):
"""Health check for business services."""
def __init__(self, user_service: IUserService, order_service: IOrderService):
self.user_service = user_service
self.order_service = order_service
def check(self) -> HealthStatus:
"""Check service health."""
try:
# Test basic service functionality
user_count = self.user_service.get_user_count()
order_count = self.order_service.get_order_count()
return HealthStatus(
healthy=True,
message=f"Services healthy: {user_count} users, {order_count} orders"
)
except Exception as e:
return HealthStatus(
healthy=False,
message=f"Service check failed: {e}"
)
Dynamic Module Loading¶
class PluginManager:
"""Manages dynamic loading of plugin modules."""
def __init__(self, plugin_dir: str):
self.plugin_dir = Path(plugin_dir)
self._loaded_plugins = {}
def load_plugin(self, plugin_name: str) -> Module:
"""Load a plugin module by name."""
if plugin_name in self._loaded_plugins:
return self._loaded_plugins[plugin_name]
plugin_path = self.plugin_dir / plugin_name / "plugin.py"
if not plugin_path.exists():
raise PluginNotFoundError(f"Plugin {plugin_name} not found")
# Load plugin module
spec = importlib.util.spec_from_file_location(
f"plugin_{plugin_name}",
plugin_path
)
plugin_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(plugin_module)
# Get plugin class
plugin_class = getattr(plugin_module, 'PluginModule')
# Load plugin configuration
config = self._load_plugin_config(plugin_name)
# Create and cache plugin
plugin = plugin_class(config)
self._loaded_plugins[plugin_name] = plugin
return plugin
def _load_plugin_config(self, plugin_name: str) -> dict:
"""Load configuration for plugin."""
config_file = self.plugin_dir / plugin_name / "config.yaml"
if config_file.exists():
with open(config_file) as f:
return yaml.safe_load(f)
return {}
Module Metrics¶
class MetricsModule(Module):
"""Module that provides metrics collection for all services."""
def configure(self, binder):
binder.bind(IMetricsCollector, PrometheusMetricsCollector())
@provider
def create_metrics_collector(self) -> IMetricsCollector:
"""Create metrics collector with module-specific metrics."""
collector = PrometheusMetricsCollector()
# Add module metrics
collector.gauge("modules_loaded", len(container._modules))
collector.counter("services_created", len(container._bindings))
collector.histogram("service_creation_time", [])
return collector
class InstrumentedModule(Module):
"""Example of instrumented module."""
def __init__(self, metrics: IMetricsCollector):
self.metrics = metrics
def configure(self, binder):
# Bind instrumented services
binder.bind(IUserService, InstrumentedUserService(self.metrics))
binder.bind(IOrderService, InstrumentedOrderService(self.metrics))
class InstrumentedUserService:
"""User service with metrics instrumentation."""
def __init__(self, metrics: IMetricsCollector):
self.metrics = metrics
self._user_service = UserService()
def create_user(self, email: str, password: str) -> User:
"""Create user with metrics."""
with self.metrics.timer("user_creation_duration"):
user = self._user_service.create_user(email, password)
self.metrics.increment("users_created")
return user
🎯 Summary¶
Module best practices ensure:
- Maintainability - Clear responsibilities and boundaries
- Testability - Easy to test in isolation and integration
- Reusability - Modules work across different applications
- Flexibility - Easy to compose and configure
- Reliability - Proper error handling and health checks
Key principles: - Single responsibility per module - Interface-based design - Comprehensive documentation - Thorough testing - Loose coupling and high cohesion
Essential practices: - Consistent naming conventions - Centralized configuration management - Dependency documentation - Health checks and monitoring - Version compatibility checking
Avoid common pitfalls: - God modules with multiple responsibilities - Configuration scattering - Tight coupling between modules - Side effects in configuration - Circular dependencies
Ready to explore framework integrations?