Skip to main content

Dependency Injection

Tasteful uses the powerful dependency-injector library to manage component relationships, lifecycles, and testing. This system is the foundation that makes Tasteful’s modular architecture possible and maintainable.
Tasteful leverages the dependency-injector library for its IoC container implementation. For complete API reference and advanced features, visit the official dependency-injector documentation.

Why Dependency Injection?

Dependency injection solves several critical problems in application architecture:

🔧 Testability

Without DI, testing becomes a nightmare of tight coupling:
# ❌ Hard to test - tightly coupled
class UserService:
    def __init__(self):
        self.db = PostgreSQLConnection()  # Always connects to real DB
        self.email = SMTPEmailService()   # Always sends real emails
    
    def create_user(self, user_data):
        user = self.db.save(user_data)
        self.email.send_welcome_email(user.email)
        return user
With DI, testing becomes elegant:
# ✅ Easy to test - dependencies injected
class UserService:
    def __init__(self, repository: UserRepository, email_service: EmailService):
        self.repository = repository
        self.email_service = email_service
    
    def create_user(self, user_data):
        user = self.repository.save(user_data)
        self.email_service.send_welcome_email(user.email)
        return user

# In tests
def test_create_user():
    mock_repo = Mock(spec=UserRepository)
    mock_email = Mock(spec=EmailService)
    service = UserService(mock_repo, mock_email)
    # Test with complete control over dependencies

🏗️ Modularity

DI enables true modularity where components can be swapped without code changes:
# Same service, different implementations
production_app = TastefulApp(
    flavors=[UserFlavor],
    repository=PostgreSQLUserRepository,
    email_service=SendGridEmailService
)

development_app = TastefulApp(
    flavors=[UserFlavor],  
    repository=InMemoryUserRepository,
    email_service=ConsoleEmailService
)

🔄 Lifecycle Management

Automatically handles singleton patterns, lazy loading, and resource cleanup.

How Tasteful’s DI Works

Tasteful uses declarative containers from the dependency-injector library. Here’s the architecture:

Container Hierarchy

# Main application container
class TastefulContainer(containers.DeclarativeContainer):
    flavors = providers.Singleton(BaseFlavor)

# Each flavor has its own container  
class BaseFlavorContainer(containers.DeclarativeContainer):
    services_factory = providers.Factory()
    repository_factory = providers.Factory()
    
    services = providers.Singleton(ServiceDispatcher, __self__)
    repository = providers.Singleton(RepositoryDispatcher, __self__)

Automatic Service Registration

When you define a flavor, Tasteful automatically registers your services using constructor-based dependency injection:
class UserController(BaseController):
    def __init__(self, user_service: UserService, email_service: EmailService):
        super().__init__(prefix="/users", tags=["users"])
        self.user_service = user_service
        self.email_service = email_service
    
    @Get("/{user_id}")
    def get_user(self, user_id: int):
        return self.user_service.get_user(user_id)

class UserFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=UserController,
            services=[UserService, EmailService],  # ← Automatically registered
            repositories=[UserRepository],         # ← Automatically registered
            config=UserConfig
        )
Behind the scenes, Tasteful:
  1. Analyzes constructor signatures to understand dependencies
  2. Uses graph-based resolution to determine dependency order
  3. Registers providers in the container automatically
  4. Injects dependencies into constructors
  5. Manages lifecycles (singletons, factories, etc.)
I