Skip to content

Dependency Registration

Registration is the process of telling the container which implementation to provide when a specific interface (protocol) is requested. FastIoC makes this incredibly simple with zero boilerplate.

Creating a Container

First, create a Container instance:

from fastioc import Container

container = Container()

That's it! Your container is ready to register dependencies.

Understanding Interfaces

In FastIoC, any type can be used as an interface - whether it's a Protocol, an abstract class, a regular class, or even a custom marker type. However, Protocols are recommended for class-based dependencies as they provide better type safety, editor support, and enable true interface-based programming following the Dependency Inversion Principle:

from typing import Protocol

class IService(Protocol):
    """This is your interface - what you'll request in endpoints"""

    def get_data(self) -> str: ...

Registering Dependencies

FastIoC supports everything that can be a dependency in FastAPI - classes, functions, generators, async generators, callable objects, and more. All of them work seamlessly with both sync and async implementations - FastAPI handles the execution automatically.

Here are the most common patterns:

Class-Based Dependencies

The most common pattern for stateful services:

from typing import Protocol

# 1. Define your interface
class IUserService(Protocol):
    def get_user(self, id: int) -> dict: ...
    async def get_user_async(self, id: int) -> dict: ...

# 2. Implement the concrete class
class UserService(IUserService):
    def __init__(self):
        self.db_connection = "postgresql://..."

    def get_user(self, id: int) -> dict:
        return {"id": id, "name": "John Doe"}

    async def get_user_async(self, id: int) -> dict:
        # You can mix sync and async methods in the same class
        return {"id": id, "name": "Jane Doe"}

# 3. Register with the container
container.add_scoped(IUserService, UserService)

Using the Same Class as Interface:

For simpler projects, you can use the class itself as both the interface and implementation (similar to NestJS):

class UserService:
    def get_user(self, id: int) -> dict:
        return {"id": id, "name": "John Doe"}

# Register the class to itself
container.add_scoped(UserService, UserService)

However, for larger projects, using separate Protocols as interfaces is considered best practice as it: - Follows the Dependency Inversion Principle (depend on abstractions, not concretions) - Makes your code more testable (easy to mock interfaces) - Provides better separation of concerns - Enables swapping implementations without changing dependent code

Function-Based Dependencies

Perfect for simple, stateless operations or direct value providers:

# 1. Define a marker type for your dependency
class UserId(int):
    """Custom type to identify this dependency"""
    ...

# 2. Create your function
def get_user_id() -> int:
    return 42

# 3. Register with the container
container.add_scoped(UserId, get_user_id)

Async Example:

class ApiKey(str):
    ...

async def fetch_api_key() -> str:
    # FastAPI handles async automatically
    return "secret-key-123"

container.add_scoped(ApiKey, fetch_api_key)

Generator-Based Dependencies

Ideal when you need setup and teardown logic (like managing database connections or file handles):

from typing import Generator

class DatabaseConnection(object):
    """Marker type for DB connection"""
    ...

def get_db_connection() -> Generator[object, None, None]:
    # Setup: acquire resource
    connection = create_connection()
    print("Connection opened")

    try:
        yield connection  # Provide the dependency
    finally:
        # Teardown: cleanup resource
        connection.close()
        print("Connection closed")

container.add_scoped(DatabaseConnection, get_db_connection)

Async Generator Example:

from typing import AsyncGenerator

class AsyncDbConnection(object):
    ...

async def get_async_db() -> AsyncGenerator[object, None]:
    # Setup
    db = await connect_async()

    try:
        yield db
    finally:
        # Cleanup
        await db.close()

container.add_scoped(AsyncDbConnection, get_async_db)

Callable Class Instances (Advanced)

For more advanced use cases, you can use callable class instances as dependencies. This pattern is useful when you need to configure a dependency with parameters or maintain state across multiple calls:

# 1. Define a callable class (with __call__ method)
class DatabaseConnectionFactory:
    def __init__(self, connection_string: str):
        self.connection_string = connection_string
        self.call_count = 0

    def __call__(self) -> dict:
        self.call_count += 1
        return {
            "connection": self.connection_string,
            "call_count": self.call_count
        }

# 2. Create an instance with configuration
db_factory = DatabaseConnectionFactory("postgresql://localhost/mydb")

# 3. Define a marker type
class DbConfig(dict):
    ...

# 4. Register the instance (not the class!)
container.add_scoped(DbConfig, db_factory)

When to use: - When you need to pass configuration to the dependency at registration time - When you need to maintain state across dependency resolutions - When you want more control over the dependency creation process

Note: You're registering the instance (the callable object), not the class itself. Each time the dependency is resolved, the __call__ method is invoked.

@app.get('/db-info')
def get_db_info(config1: DbConfig, config2: DbConfig):
    # First call: {"connection": "postgresql://...", "call_count": 1}
    # Second call: {"connection": "postgresql://...", "call_count": 2}
    # The instance's state is maintained!
    return {"config1": config1, "config2": config2}

This pattern gives you the flexibility of functions with the state management of classes.

Registration Methods

FastIoC provides three registration methods that control dependency lifetimes:

container.add_singleton(IService, ServiceImpl)   # One instance per application
container.add_scoped(IService, ServiceImpl)      # One instance per request
container.add_transient(IService, ServiceImpl)   # New instance every time

We'll explore these lifetimes in detail in the Dependency Lifetimes chapter.

Re-registering Dependencies

If you register multiple implementations for the same interface, the last registration always replaces previous ones:

# First registration
container.add_scoped(IUserService, UserServiceV1)

# Second registration - replaces the first one
container.add_scoped(IUserService, UserServiceV2)  # This is what will be used

# Third registration - replaces the second one
container.add_scoped(IUserService, UserServiceV3)  # This is what will actually be injected

When a dependency is re-registered: - The previous implementation is completely forgotten - Only the last registered implementation is used - No warning or error is raised

When to use this:

This behavior is mainly useful when you accidentally register the same interface twice, or when you intentionally want to replace a registration during initial setup.

When NOT to use this:

❌ For testing/mocking - Use Dependency Overrides instead (proper way to mock for tests)

❌ For environment-specific configs - Use Dependency Overrides instead (proper way to handle dev/prod)

Re-registration works, but dependency overrides are the recommended best practice for replacing implementations in different contexts (testing, environments, etc.).

Next Steps