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?