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 pattern | Injex 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 unit | add_scoped(X) |
| factory returning a prebuilt object | add_instance(X, obj) |
| custom construction logic | add_singleton_factory / add_transient_factory |
| two implementations of one interface | add_*(X, Impl, name="a") |
| constructor reads dependencies positionally | unchanged — 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:
- add a container alongside the existing factories module;
- move leaf services first (clients, repositories), keep
get_*wrappers that callcontainer.resolve(...); - move higher-level use cases once their dependencies are registered;
- replace
monkeypatchcalls withoverride()as you touch each test; - delete the old
get_*functions when nothing imports them.
At every step the application classes stay unchanged — only the wiring moves. See the
FastAPI boundary guide for keeping Depends at the HTTP edge.