Module Best Practices - InjectQ Documentation
Author: InjectQ Contributors
Description: Modern Python dependency injection library

Skip to content

# 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?