Skip to content

Advanced Container Service Usage

Advanced patterns for complex applications including testing, custom tokens, lifecycle management, and sophisticated dependency scenarios.

Custom Token Keys

Use tokens when you need multiple implementations of the same service type or string-based service keys.

Multiple Database Connections

from fastedgy.dependencies import Token, register_service, Inject

# Define custom tokens
PRIMARY_DB = Token[DatabaseService]("primary_db")
ANALYTICS_DB = Token[DatabaseService]("analytics_db")
CACHE_DB = Token[DatabaseService]("cache_db")

# Register different database instances
register_service(
    DatabaseService("postgresql://primary-server/app"),
    PRIMARY_DB
)
register_service(
    DatabaseService("postgresql://analytics-server/warehouse"),
    ANALYTICS_DB
)
register_service(
    DatabaseService("redis://cache-server:6379"),
    CACHE_DB
)

# Use specific databases in endpoints
@router.get("/analytics")
async def get_analytics(
    primary_db: DatabaseService = Inject(PRIMARY_DB),
    analytics_db: DatabaseService = Inject(ANALYTICS_DB)
):
    users = primary_db.query("SELECT * FROM users")
    analytics_db.query("INSERT INTO user_analytics ...")
    return {"user_count": len(users)}

Environment-Based Services

# Define environment-specific tokens
DEV_EMAIL = Token[EmailService]("dev_email")
PROD_EMAIL = Token[EmailService]("prod_email")

def setup_email_services():
    if os.getenv("ENVIRONMENT") == "production":
        register_service(SMTPEmailService("smtp.mailgun.org"), PROD_EMAIL)
        register_service(SMTPEmailService("smtp.mailgun.org"), EmailService)  # Default
    else:
        register_service(ConsoleEmailService(), DEV_EMAIL)
        register_service(ConsoleEmailService(), EmailService)  # Default

@router.post("/notify")
async def notify_user(
    email_service: EmailService = Inject(EmailService)  # Gets environment-appropriate service
):
    return email_service.send_email("user@example.com", "Hello", "World")

Advanced Factory Patterns

Factory Functions with Dependencies

Create factories that inject other services during construction:

def create_payment_processor(
    config: AppConfig = Inject(AppConfig),
    logger: LoggerService = Inject(LoggerService),
    db: DatabaseService = Inject(DatabaseService)
) -> PaymentProcessor:
    """Factory that configures payment processor based on environment."""

    if config.environment == "production":
        processor = StripePaymentProcessor(
            api_key=config.stripe_api_key,
            logger=logger
        )
    else:
        processor = MockPaymentProcessor(logger=logger)

    # Common initialization
    processor.setup_webhooks()
    processor.register_event_handlers(db)

    return processor

# Register the factory
register_service(create_payment_processor, PaymentProcessor)

# Use in endpoints
@router.post("/payments")
async def process_payment(
    amount: float,
    processor: PaymentProcessor = Inject(PaymentProcessor)
):
    return processor.charge(amount)

Conditional Service Creation

def create_notification_service(
    config: AppConfig = Inject(AppConfig)
) -> NotificationService:
    """Create notification service with multiple channels based on config."""

    channels = []

    if config.email_enabled:
        channels.append(EmailNotificationChannel(config.smtp_host))

    if config.sms_enabled:
        channels.append(SMSNotificationChannel(config.twilio_key))

    if config.slack_enabled:
        channels.append(SlackNotificationChannel(config.slack_webhook))

    return NotificationService(channels)

register_service(create_notification_service, NotificationService)

Service Lifecycle Management

Startup and Shutdown Hooks

Manage service initialization and cleanup using FastAPI's lifespan system:

from contextlib import asynccontextmanager
from fastedgy import FastEdgy

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Startup: initialize services

    # Register services with complex initialization
    database = DatabaseService(DATABASE_URL)
    await database.connect()
    register_service(database)

    # Initialize background services
    task_processor = TaskProcessor()
    await task_processor.start()
    register_service(task_processor)

    # Register dependent services
    register_service(UserService)  # Will use registered DatabaseService
    register_service(EmailService)

    try:
        yield  # Application runs here
    finally:
        # Shutdown: cleanup services
        db = get_service(DatabaseService)
        await db.disconnect()

        processor = get_service(TaskProcessor)
        await processor.shutdown()

app = FastEdgy(lifespan=lifespan)

Health Checks and Monitoring

class HealthCheckService:
    def __init__(self,
                 db: DatabaseService,
                 cache: CacheService,
                 external_api: ExternalAPIService):
        self.db = db
        self.cache = cache
        self.external_api = external_api

    async def check_health(self):
        checks = {}

        # Database check
        try:
            await self.db.ping()
            checks["database"] = {"status": "healthy"}
        except Exception as e:
            checks["database"] = {"status": "unhealthy", "error": str(e)}

        # Cache check
        try:
            await self.cache.ping()
            checks["cache"] = {"status": "healthy"}
        except Exception as e:
            checks["cache"] = {"status": "unhealthy", "error": str(e)}

        # External API check
        try:
            await self.external_api.ping()
            checks["external_api"] = {"status": "healthy"}
        except Exception as e:
            checks["external_api"] = {"status": "unhealthy", "error": str(e)}

        return checks

# Auto-resolved with all dependencies
@router.get("/health")
async def health_check(health: HealthCheckService = Inject(HealthCheckService)):
    return await health.check_health()

Testing and Mocking

Service Override for Testing

Replace real services with mocks during testing:

import pytest
from unittest.mock import Mock
from fastedgy.dependencies import register_service, unregister_service

class MockEmailService:
    def __init__(self):
        self.sent_emails = []

    def send_email(self, to: str, subject: str, body: str):
        email = {"to": to, "subject": subject, "body": body}
        self.sent_emails.append(email)
        return {"sent": True, "id": f"mock-{len(self.sent_emails)}"}

class MockDatabaseService:
    def __init__(self):
        self.data = {}
        self.queries = []

    def query(self, sql: str):
        self.queries.append(sql)
        return {"mock": "result"}

    def create_user(self, user_data: dict):
        user_id = len(self.data) + 1
        user = {"id": user_id, **user_data}
        self.data[user_id] = user
        return user

@pytest.fixture
def mock_services():
    """Replace services with mocks for testing."""

    # Create mocks
    mock_email = MockEmailService()
    mock_db = MockDatabaseService()

    # Override services
    register_service(mock_email, EmailService, force=True)
    register_service(mock_db, DatabaseService, force=True)

    yield {
        "email": mock_email,
        "database": mock_db
    }

    # Cleanup
    unregister_service(EmailService)
    unregister_service(DatabaseService)

def test_user_registration(mock_services):
    """Test user registration with mocked services."""

    # Test the endpoint
    response = client.post("/register", json={
        "email": "test@example.com",
        "name": "Test User"
    })

    assert response.status_code == 200

    # Verify email was sent
    assert len(mock_services["email"].sent_emails) == 1
    assert mock_services["email"].sent_emails[0]["to"] == "test@example.com"

    # Verify user was created
    assert len(mock_services["database"].data) == 1
    assert mock_services["database"].data[1]["email"] == "test@example.com"

Integration Testing with Real Services

@pytest.fixture(scope="session")
def test_database():
    """Create a test database for integration tests."""

    # Setup test database
    test_db_url = "postgresql://test:test@localhost/test_db"
    test_db = DatabaseService(test_db_url)

    # Initialize schema
    test_db.create_tables()

    # Override the service
    register_service(test_db, DatabaseService, force=True)

    yield test_db

    # Cleanup
    test_db.drop_tables()
    unregister_service(DatabaseService)

def test_user_service_integration(test_database):
    """Test UserService with real database."""

    user_service = get_service(UserService)  # Uses test database

    # Test user creation
    user = user_service.create_user({
        "email": "integration@test.com",
        "name": "Integration Test"
    })

    assert user["id"] is not None
    assert user["email"] == "integration@test.com"

    # Test user retrieval
    retrieved_user = user_service.get_user(user["id"])
    assert retrieved_user["email"] == "integration@test.com"

Testing Factory Services

def test_payment_processor_factory():
    """Test that payment processor factory creates correct implementation."""

    # Test production configuration
    prod_config = AppConfig()
    prod_config.environment = "production"
    prod_config.stripe_api_key = "test_key"

    register_service(prod_config, AppConfig, force=True)

    processor = get_service(PaymentProcessor)
    assert isinstance(processor, StripePaymentProcessor)
    assert processor.api_key == "test_key"

    # Test development configuration
    dev_config = AppConfig()
    dev_config.environment = "development"

    register_service(dev_config, AppConfig, force=True)

    # Force recreation of payment processor
    unregister_service(PaymentProcessor)

    processor = get_service(PaymentProcessor)
    assert isinstance(processor, MockPaymentProcessor)

Complex Dependency Scenarios

Circular Dependencies (Advanced Resolution)

Handle circular dependencies with lazy loading:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from services.order_service import OrderService

class UserService:
    def __init__(self, db: DatabaseService):
        self.db = db
        self._order_service = None

    @property
    def order_service(self) -> 'OrderService':
        if self._order_service is None:
            from fastedgy.dependencies import get_service
            from services.order_service import OrderService
            self._order_service = get_service(OrderService)
        return self._order_service

    def get_user_orders(self, user_id: str):
        return self.order_service.get_orders_for_user(user_id)

class OrderService:
    def __init__(self, db: DatabaseService, user_service: UserService):
        self.db = db
        self.user_service = user_service

    def create_order(self, order_data: dict):
        user = self.user_service.get_user(order_data["user_id"])
        # Create order logic...

Dynamic Service Resolution

class ServiceRegistry:
    """Dynamic service locator for plugin-like architecture."""

    def __init__(self):
        self.handlers = {}

    def register_handler(self, event_type: str, handler_class: type):
        self.handlers[event_type] = handler_class

    def get_handler(self, event_type: str):
        handler_class = self.handlers.get(event_type)
        if not handler_class:
            raise ValueError(f"No handler registered for {event_type}")

        # Use Container Service to resolve handler dependencies
        from fastedgy.dependencies import get_service
        return get_service(handler_class)

# Register dynamic service
register_service(ServiceRegistry())

# Register event handlers
def setup_event_handlers():
    registry = get_service(ServiceRegistry)

    registry.register_handler("user.created", UserCreatedHandler)
    registry.register_handler("order.completed", OrderCompletedHandler)
    registry.register_handler("payment.failed", PaymentFailedHandler)

class EventProcessor:
    def __init__(self, registry: ServiceRegistry):
        self.registry = registry

    async def process_event(self, event_type: str, event_data: dict):
        handler = self.registry.get_handler(event_type)  # Auto-resolved with dependencies
        return await handler.handle(event_data)

Performance Optimization

Lazy Loading Services

class ExpensiveService:
    """Service with expensive initialization."""

    def __init__(self, config: AppConfig):
        print("Initializing expensive service...")
        # Expensive initialization here
        self.expensive_resource = self._load_expensive_resource()

    def _load_expensive_resource(self):
        # Simulate expensive operation
        time.sleep(2)
        return "expensive resource"

# Don't register directly - use factory for lazy loading
def create_expensive_service(config: AppConfig = Inject(AppConfig)):
    return ExpensiveService(config)

register_service(create_expensive_service, ExpensiveService)

# Service is only created when first accessed
@router.get("/expensive-operation")
async def expensive_operation(
    service: ExpensiveService = Inject(ExpensiveService)  # Created here if first time
):
    return service.expensive_resource

Caching and Memoization

from functools import lru_cache

class CacheService:
    def __init__(self):
        self._cache = {}

    @lru_cache(maxsize=128)
    def get_expensive_data(self, key: str):
        # Expensive computation
        return f"expensive_result_for_{key}"

    def clear_cache(self):
        self.get_expensive_data.cache_clear()

# Singleton ensures cache is shared across requests
cached_service: CacheService = Inject(CacheService)

Error Handling and Debugging

Service Registration Validation

from contextlib import asynccontextmanager
from fastedgy.app import FastEdgy
from fastedgy.dependencies import get_service

def validate_services():
    """Validate that all required services are properly registered."""

    required_services = [
        DatabaseService,
        EmailService,
        CacheService,
        PaymentProcessor
    ]

    missing_services = []

    for service_class in required_services:
        try:
            get_service(service_class)
        except LookupError:
            missing_services.append(service_class.__name__)

    if missing_services:
        raise RuntimeError(f"Missing required services: {missing_services}")

# Optional custom lifespan for service validation
@asynccontextmanager
async def lifespan(app: FastEdgy):
    setup_services()
    validate_services()  # Ensure all services are available
    yield

# FastEdgy handles DB and core services automatically
app = FastEdgy(
    title="My App",
    lifespan=lifespan,  # Optional - only for custom validation logic
)

Custom Error Handling

class ServiceError(Exception):
    """Base exception for service-related errors."""
    pass

class ServiceConfigurationError(ServiceError):
    """Raised when a service is misconfigured."""
    pass

def create_database_service(config: AppConfig = Inject(AppConfig)):
    """Factory with error handling."""

    if not config.database_url:
        raise ServiceConfigurationError(
            "DATABASE_URL is required but not configured"
        )

    try:
        return DatabaseService(config.database_url)
    except Exception as e:
        raise ServiceConfigurationError(f"Failed to create database service: {e}")

register_service(create_database_service, DatabaseService)

Next Steps

This covers the advanced usage patterns. For implementation details and architectural information:

Technical Details →

Or return to simpler guides:

Advanced Quick Reference

# Custom tokens
PRIMARY_DB = Token[DatabaseService]("primary")
register_service(db_instance, PRIMARY_DB)
db: DatabaseService = Inject(PRIMARY_DB)

# Factories with dependencies
def create_service(dep: DepService = Inject(DepService)):
    return MyService(dep, custom_config)
register_service(create_service, MyService)

# Testing overrides
register_service(mock_service, RealService, force=True)
# ... run tests ...
unregister_service(RealService)

# Service validation
try:
    service = get_service(MyService)
except LookupError:
    print("Service not registered")