// main.ts: composition root
const pool = new Pool({ connectionString: config.dbUrl });
const orders = new PgOrderRepository(pool);
const notifier = new SesNotifier(new SESClient({}));
const placeOrder = new PlaceOrderUseCase(orders, notifier);
const app = createHttpApp({ placeOrder });
app.listen(config.port);
No. DI (dependency injection) only means that objects receive their dependencies instead of creating them. You can wire them with new at the composition root, as above. The composition root is the one place near the entry point where objects are created and connected. This is full DI, with no framework.
Manual DI has real advantages:
- The wiring is plain code that the compiler checks.
- There is no decorator metadata or reflection. Reflection means reading type information at runtime.
- A missing dependency is a type error, not a runtime resolution failure.
It works well for small and medium services.
An IoC (Inversion of Control) container is a library that creates and wires objects for you. It pays off as the object graph grows. A container:
- resolves constructor parameters automatically
- manages lifetimes: singleton, per request or transient (a new instance every time)
- handles modules and dynamic configuration
- lets you override providers in tests
The costs of a container:
- more indirection, so the wiring is harder to follow
- runtime errors when a provider is missing
- coupling to the framework's conventions
Either way, the business classes should look the same. Their constructor parameters use abstraction types (interfaces), and they know nothing about how they are wired.
- DI is a technique. A container only automates it.
- Manual DI is explicit, checked at compile time and uses no reflection.
- A container gives auto-wiring, lifetimes, scoping and test overrides.
- Business classes stay unaware of how they are wired.