Most Python web frameworks resolve dependencies per-handler: a route declares what it needs and the framework supplies it. That works, and for a small service it is hard to beat.
It stops working when you want to know what depends on what without reading every handler.
The module graph is the answer#
A provider belongs to a module. A module states what it imports and what it exports. Those two lists are the entire visibility rule:
- A provider is visible inside its own module
- A provider is visible elsewhere only if it is exported and the other module imports the owner
So the dependency structure is a graph you can read off the declarations. Since
0.0.2 you can also print it:
$ bustan graph
AppModule
├── TaskModule
│ ├── provides TaskService
│ └── exports TaskService
└── ReportModule
├── imports TaskModule
└── provides ReportService
Why private by default#
Everything-global is more convenient for about three weeks. After that any provider is reachable from anywhere, the graph is complete, and "what breaks if I change this" has no answer short of running everything.
Private-by-default keeps the graph sparse, and adding an edge becomes a deliberate act visible in a diff.
Resolution is constructor-based, and eager#
The container reads constructor annotations. @Injectable() marks a class the
container may build, so a missing registration fails at startup rather than
mysteriously at request time.
The graph is walked once at startup, providers are constructed in dependency order, and cycles are errors:
BustanResolutionError: circular dependency
TaskService → ReportService → TaskService
Eager resolution is the point. A framework that resolves lazily turns a structural mistake into an intermittent one.
Scopes, and the rule that surprises people#
0.0.2 adds request scope. With it comes a constraint the container enforces:
A request-scoped provider cannot be injected into a singleton.
If it could, the singleton would capture the first request's instance and hold it forever — a bug that shows up as one user seeing another's data, weeks later, under load. Rejecting it at startup is unkind and correct.
What this costs#
Ceremony. A three-endpoint service is more code in Bustan than in a bare router, and if that is what you are building, the bare router is the better tool.
The trade pays off when a second person joins or a module has to move. Structure is a cost paid early against an unbounded cost later — and if the codebase never gets there, you simply paid.