Guide

Move a factories module to Injex.

Many Python apps start with a hand-written factories.py that builds the service graph. When the same graph is rebuilt across API startup, CLI commands, workers, and tests, it drifts out of sync. This guide moves that wiring to Injex without changing a single application class.

Keep manual factories while they stay small and local. Move to Injex when the same graph repeats across several entrypoints and you want one validated composition root.

Before: manual factories

A typical factories module wires everything by hand and is imported by each entrypoint:

# factories.py
from functools import lru_cache


@lru_cache
def get_settings() -> Settings:
    return load_settings()


@lru_cache
def get_api_client() -> ApiClient:
    return ApiClient(get_settings())


def get_user_repository() -> UserRepository:
    return UserRepository(get_api_client())


def get_register_user() -> RegisterUser:
    return RegisterUser(get_user_repository(), get_email_sender())

This works, but lifetimes are encoded ad hoc, there is no single place to validate the graph, and every new service adds another get_* function.

After: one container at the composition root

Application classes stay the same — plain constructors with type hints. Only wiring changes:

# composition.py
from injex import Container


def build_container() -> Container:
    container = Container()
    container.add_instance(Settings, load_settings())
    container.add_singleton(ApiClient)
    container.add_transient(UserRepository)
    container.add_transient(EmailSender)
    container.add_transient(RegisterUser)
    container.assert_valid()
    return container

Each entrypoint builds the container once and resolves what it needs:

container = build_container()
register_user = container.resolve(RegisterUser)

Mapping table

Manual factory patternInjex registration
@lru_cache factory (one shared instance)add_singleton(X)
plain factory (new each call)add_transient(X)
per-request / per-job object reused in one unitadd_scoped(X)
factory returning a prebuilt objectadd_instance(X, obj)
custom construction logicadd_singleton_factory / add_transient_factory
two implementations of one interfaceadd_*(X, Impl, name="a")
constructor reads dependencies positionallyunchanged — resolved from type hints

Tests: from patching to overrides

Instead of patching the factory module, scope the swap to a with block. Production registrations stay untouched and the override restores automatically:

def test_register():
    container = build_container()
    fake = FakeEmailSender()
    with container.override(EmailSender, instance=fake):
        register_user = container.resolve(RegisterUser)
        register_user.execute("ada@example.com")
    assert fake.sent_to == ["ada@example.com"]

Incremental migration

You do not have to convert everything at once:

At every step the application classes stay unchanged — only the wiring moves. See the FastAPI boundary guide for keeping Depends at the HTTP edge.