Three years. That's how long we ran InversifyJS in production at a medium-sized Typescript SaaS. It worked. But working and being good are two diff...
For further actions, you may consider blocking this person and/or reporting abuse
The 1114-line binding file you screenshotted is the kind of thing
I keep an eye out for in our FastAPI Python codebase too. Our
equivalent is the dependency registry under
app/api/deps.py. Wecaught it before it got past 200 lines by treating "if you're
touching this file you're touching three places" as a design smell.
What worked for us: we moved most manually-bound classes to
function-level
Depends(get_x)callables. Lower ceremony, the typechecker sees through them. We still have a small central registry
for things with genuinely cross-cutting state (db session, current
user), but it stays small because adding a new service doesn't
require touching it.
Curious about your decorator discovery: how do you handle two
implementations of the same interface (test fake vs prod), where
InversifyJS would let you bind a different concrete class per env?
Hey Vadym, this is how we handle multiple implementations in lazy-di:
The classes can be in different files, of course, but this raises another problem.
Business logic is going to use the abstract class, so the two other implementations might not be imported at all in the code. We fixed this by adding a scan method on a container that automatically scans and imports dependencies, this is how it looks:
scan() makes sense as a workaround for the tree-shaking problem. In
Python this doesn't bite because module imports are eager and
decorators fire at import time. If you import the file that defines
StripeGateway, the @Implements call runs and registers it. So our
equivalent setup needs nothing fancier than a gateways/init.py
that re-exports everything, imported once at app boot.
The TS-specific challenge is bundlers wanting to drop imports they
can't see being used. scan() inverts the import direction (container
pulls implementations in instead of business code pulling them in).
Does it slow cold starts noticeably in a serverless context with a
few hundred decorated classes? That's the part I'd worry about.
Decorators fire at runtime in TypeScript as well.
The idea is that an implementation is not bound to the abstract class directly after importing, that is delegated to the @Implements decorator.
And the @Implements decorator itself decides whether to bind it or not based on the condition passed by the user. So the @Implements can be triggered, but doesn't do anything if the condition provided by the developer is false.
It definitely adds some latency to the cold start, but in our case, it is acceptable.
This is the data from our codebase:
Got it. So container.scan() is essentially what binding files were, just implicit — same coupling, less visible. The Inversify pain wasn't bind-files per se but lack of typed-token enforcement. lazy-di solves the second but trades the first for scan cost. Tradeoff makes sense for app-tier where cold start doesn't matter, less so for serverless. Thanks for the thoughtful answers.
I understand the frustration with InversifyJS, but looking at the architecture of
lazy-di, it seems you were inspired by the worst anti-patterns of popular DI containers. You essentially took all the bad parts of existing solutions while completely ignoring the advanced type-level capabilities of modern TypeScript.By trying to simplify the syntax,
lazy-diinherited the same old "diseases" that make production DI a nightmare:Ignoring Type Safety (Stringly-typed graphs)
Relying on manual casting like
container.resolve<MyService>('token')defeats the entire purpose of TypeScript. If I change the dependency type or make a typo, TS will happily compile it, and it will crash in runtime.Implicit Magic (The Proxy Trap)
Using standard "transparent" proxies for lazy evaluation is an illusion of simplicity. Proxies kill V8 performance by deoptimizing Inline Caches, break
instanceofchecks, and makeconsole.logdebugging a nightmare.Captive Dependencies (Scope Leaks)
This is the most critical bug in any DI: injecting a short-lived
ScopedorTransientservice into a long-livedSingleton. Popular containers let this slip through, turning scoped instances into unintended singletons (causing cross-request data leaks).Lack of Lifecycle Management
Modern JS/TS has Explicit Resource Management (
using/await usingwithSymbol.asyncDispose). A mature DI container must own its instances and tear them down in strict LIFO (reverse-creation) order. Without this, you get hanging DB connections and memory leaks.It’s great that
lazy-diis lightweight, but it trades crucial compile-time safety and engine performance for a few lines of less boilerplate. Modern TypeScript can do much better than this.Hey Viacheslav Kabanov, I think there’s some confusion. I believe you have looked at the wrong repo.
There is already a DI container called exactly "lazy-di" on npm, but it’s not mine. It’s old and no longer maintained.
My package is called "@lazy-di/core" on npm. Both links (GitHub and npm) are provided at the end of the blog post.
Hey Ayoub,
First of all — sorry for the confusion. You’re right, I initially looked at the wrong “lazy-di” package on npm (the old unmaintained one). I’ve now carefully reviewed @lazy-di/core from your repository.
Your frustration with InversifyJS is very relatable — especially the constant merge conflicts around the central binding file.
That said, here’s where I still stand by my core criticism:
Overall, lazy-di is clearly a solid improvement over classic InversifyJS for developers who prioritize minimal ceremony and nice DX. Respect for shipping it and writing the blog post — posts like “I got tired of X after Y years in production” are the most valuable kind.
@maxrendel -- the scope-leak point is the one I underweighted in my reply.
We hit a structurally similar issue in a different stack (FastAPI Depends()
with mutable singletons): ambient resolution = ambient bugs. inferdi link
appreciated, will dig in.
it's more type safe and easy to understand for every one
This was a great read. Really helpful solution to a critical problem.