Skip to main content

Overview

Soft-Bee API uses the dependency-injector library to manage dependencies, enabling loose coupling, testability, and maintainable code.
Dependency Injection (DI) is a design pattern where objects receive their dependencies from external sources rather than creating them internally.

Why Dependency Injection?

Loose Coupling

Components depend on interfaces, not concrete implementations

Testability

Easy to replace real implementations with mocks for testing

Flexibility

Swap implementations without changing code (e.g., PostgreSQL → MongoDB)

Single Responsibility

Objects focus on their logic, not creating dependencies

Container Structure

Dependencies are organized in containers defined in src/core/dependencies/containers.py:
src/core/dependencies/containers.py
from dependency_injector import containers, providers

class AuthContainer(containers.DeclarativeContainer):
    """Container for auth feature dependencies"""
    
    # Configuration
    config = providers.Configuration()
    
    # External dependencies
    db_session = providers.Dependency()
    
    # Repositories
    user_repository = providers.Factory(
        UserRepositoryImpl,
        db_session=db_session
    )
    
    # Services
    password_hasher = providers.Singleton(
        PasswordHasher,
        algorithm=config.auth.password_algorithm
    )
    
    jwt_service = providers.Singleton(
        JWTService,
        secret_key=config.auth.jwt_secret_key,
        algorithm=config.auth.jwt_algorithm,
        issuer=config.auth.jwt_issuer,
        audience=config.auth.jwt_audience
    )
    
    # Use Cases
    login_use_case = providers.Factory(
        LoginUserUseCase,
        user_repository=user_repository,
        token_service=jwt_service,
        password_hasher=password_hasher
    )
    
    register_use_case = providers.Factory(
        RegisterUserUseCase,
        user_repository=user_repository,
        password_hasher=password_hasher
    )

class MainContainer(containers.DeclarativeContainer):
    """Main application container"""
    
    config = providers.Configuration()
    db_session = providers.Dependency()
    
    # Feature containers
    auth = providers.Container(
        AuthContainer,
        db_session=db_session,
        config=config.auth
    )

Provider Types

The library offers different provider types for different needs:
Creates a new instance every time it’s called. Use for objects with state that shouldn’t be shared.
user_repository = providers.Factory(
    UserRepositoryImpl,
    db_session=db_session
)
When to use: Repositories, use cases, or any stateful object that should be created fresh for each request.
Creates one instance and reuses it. Use for stateless services.
jwt_service = providers.Singleton(
    JWTService,
    secret_key=config.auth.jwt_secret_key
)
When to use: Password hashers, JWT services, email services, or any stateless utility.
Provides configuration values from various sources.
config = providers.Configuration()
# Access: config.auth.jwt_secret_key
When to use: Application settings, environment variables, feature flags.
Placeholder for external dependencies that will be provided by parent containers.
db_session = providers.Dependency()
When to use: Database sessions, app context, or anything provided by the framework.

Injection in Endpoints

Use the @inject decorator to inject dependencies into Flask routes:
src/features/auth/presentation/api/v1/endpoints/auth.py
from flask import Blueprint, request, jsonify
from dependency_injector.wiring import inject, Provide
from .....application.use_cases.login_user import LoginUserUseCase
from src.core.dependencies.containers import MainContainer as Container

auth_bp = Blueprint('auth_v1', __name__, url_prefix='/api/v1/auth')

@auth_bp.route('/login', methods=['POST'])
@inject
def login(
    login_use_case: LoginUserUseCase = Provide[Container.auth.login_use_case]
):
    """Login endpoint with dependency injection"""
    data = request.get_json()
    
    # Convert to DTO
    login_request = LoginRequestDTO(**data)
    
    # Execute use case (injected automatically)
    result, error = login_use_case.execute(login_request)
    
    if error:
        return jsonify({"error": error}), 401
    
    return jsonify(result), 200
The @inject decorator automatically resolves dependencies using the Provide[...] syntax. You don’t need to manually create or pass dependencies!

How It Works

1

Decorator applies

The @inject decorator intercepts function calls
2

Dependencies resolved

When the route is called, Provide[Container.auth.login_use_case] tells the container to create/fetch the LoginUserUseCase
3

Nested dependencies resolved

The container also resolves the use case’s dependencies (repository, token service, password hasher)
4

Function executes

The route function receives the fully-constructed use case and executes normally

Container Initialization

Initialize containers when the Flask app starts:
app.py
from flask import Flask
from src.core.dependencies.containers import MainContainer
from src.core.database.db import get_db

def create_app():
    app = Flask(__name__)
    
    # Create container
    container = MainContainer()
    
    # Configure container
    container.config.from_dict({
        'auth': {
            'jwt_secret_key': app.config['JWT_SECRET_KEY'],
            'jwt_algorithm': app.config['JWT_ALGORITHM'],
            'jwt_issuer': 'soft-bee-api',
            'jwt_audience': 'soft-bee-users',
            'password_algorithm': 'bcrypt'
        }
    })
    
    # Provide database session
    container.db_session.override(get_db)
    
    # Wire container with modules
    container.wire(modules=[
        'src.features.auth.presentation.api.v1.endpoints.auth'
    ])
    
    app.container = container
    
    return app
Wiring tells the container which modules contain @inject decorators so it can resolve dependencies in those modules.

Testing with Dependency Injection

DI makes testing incredibly easy by allowing you to override dependencies with mocks:
tests/test_login.py
import pytest
from unittest.mock import Mock
from src.core.dependencies.containers import MainContainer
from src.features.auth.application.use_cases.login_user import LoginUserUseCase

@pytest.fixture
def container():
    """Create test container with mocked dependencies"""
    container = MainContainer()
    
    # Mock repository
    mock_repository = Mock()
    mock_repository.find_by_email.return_value = mock_user
    container.auth.user_repository.override(mock_repository)
    
    # Mock token service
    mock_token_service = Mock()
    mock_token_service.create_access_token.return_value = 'mock_token'
    container.auth.jwt_service.override(mock_token_service)
    
    return container

def test_login_success(container):
    """Test successful login"""
    # Get use case with mocked dependencies
    use_case = container.auth.login_use_case()
    
    # Execute
    result, error = use_case.execute(LoginRequestDTO(
        email='test@example.com',
        password='password123'
    ))
    
    # Assert
    assert error is None
    assert result.access_token == 'mock_token'
Use .override() to replace real implementations with mocks. This lets you test use cases without a database or external services!

Dependency Graph Example

Here’s how dependencies flow for a login request:
  1. Endpoint requests LoginUserUseCase
  2. Container creates the use case with its dependencies:
    • UserRepository (with database session)
    • JWTService (singleton)
    • PasswordHasher (singleton)
  3. All dependencies are automatically resolved and injected

Best Practices

Use Interfaces

Depend on interfaces (abstract classes) not concrete implementations

Keep Containers Organized

One container per feature for better organization

Singleton for Stateless

Use Singleton for services without state (JWT, password hasher)

Factory for Stateful

Use Factory for objects with state (repositories, use cases)

Don’t

  • Don’t create dependencies manually inside classes (e.g., self.repo = UserRepository())
  • Don’t use global singletons outside the container
  • Don’t mix business logic with dependency creation
  • Don’t forget to wire modules that use @inject

Do

  • Inject all dependencies through constructors
  • Define dependencies in containers
  • Use @inject for route handlers
  • Override dependencies in tests
  • Keep containers close to features they serve

Advanced: Multiple Feature Containers

As your application grows, organize containers by feature:
class HivesContainer(containers.DeclarativeContainer):
    """Container for hives feature"""
    config = providers.Configuration()
    db_session = providers.Dependency()
    
    hive_repository = providers.Factory(
        HiveRepositoryImpl,
        db_session=db_session
    )
    
    create_hive_use_case = providers.Factory(
        CreateHiveUseCase,
        hive_repository=hive_repository
    )

class MainContainer(containers.DeclarativeContainer):
    """Main container with all features"""
    config = providers.Configuration()
    db_session = providers.Dependency()
    
    # Feature containers
    auth = providers.Container(AuthContainer, db_session=db_session)
    hives = providers.Container(HivesContainer, db_session=db_session)
    apiaries = providers.Container(ApiariesContainer, db_session=db_session)
This keeps each feature’s dependencies isolated and maintainable.

Clean Architecture

See how DI fits into the architecture layers

Project Structure

Understand where containers live in the project

Testing Guide

Learn how to test with mocked dependencies

Build docs developers (and LLMs) love