Composition Roots and Configuration Boundaries
What This Concept Is
A composition root is the single place in an application where the entire object graph is wired together -- where new for real dependencies actually happens. Outside the composition root, code accepts its collaborators through DI and never constructs them.
Where the composition root lives depends on the runtime:
- CLI apps:
main(or the entry function) - HTTP services: the server startup module (e.g.,
createApp()) - desktop/mobile apps: the application bootstrap method
- libraries: there is no composition root -- libraries never wire graphs for their consumers
Paired with the composition root is a configuration boundary: the one layer that knows about environment variables, config files, command-line flags, and turns them into typed values the rest of the system consumes.
Why It Matters Here
Composition Root is the most overlooked pattern in this module and the one that makes every other pattern in it tractable. Without a clear composition root:
- Adapter/Facade/Decorator/Proxy cannot be chained in different ways in different environments.
- Test doubles cannot be injected cleanly.
- Configuration leaks into domain code.
- DI drifts into service-locator land.
With a clear composition root:
- production, test, and tool runs reuse the same domain code with different wirings
- one file is the exhaustive answer to "what depends on what in this app"
- environment configuration has one entry point; the rest of the code depends on typed values, not strings
Concrete Example
// src/app/compositionRoot.ts -- the ONLY place "new" and config live
import { loadConfig } from './config';
import { SystemClock } from '../infra/systemClock';
import { HttpStripeGateway } from '../infra/httpStripeGateway';
import { PostgresOrderRepo } from '../infra/postgresOrderRepo';
import { RetryingGateway } from '../infra/retryingGateway';
import { LoggingGateway } from '../infra/loggingGateway';
import { OrderService } from '../domain/orderService';
export function buildApp(env: NodeJS.ProcessEnv) {
const cfg = loadConfig(env); // strings -> typed config
const clock = new SystemClock();
const rawPay = new HttpStripeGateway(cfg.stripeKey);
const payment = new LoggingGateway( // Decorator
new RetryingGateway(rawPay, 3), // Decorator
);
const repo = new PostgresOrderRepo(cfg.dbUrl);
const orders = new OrderService(payment, repo, clock);
return { orders, /* other services */ };
}
// tests/fakes/buildTestApp.ts
export function buildTestApp() {
const clock = new FixedClock('2026-01-01T00:00:00Z');
const payment = new FakePaymentGateway();
const repo = new InMemoryOrderRepo();
return { orders: new OrderService(payment, repo, clock) };
}
// src/main.ts
const app = buildApp(process.env);
app.orders.place( /* ... */ );
Structural sketch:
+------------------------+
| Composition Root |
| (main / startup) |
+-----------+------------+
| wires
v
+------------------------+ +------------------+
| Domain services |<----->| Infra adapters |
| (no new, no config) | DI | (one per source) |
+------------------------+ +------------------+
The domain block has no new statements for external collaborators and no string environment lookups.
Common Confusion / Misconception
- A DI container is not a composition root. The container is a tool some teams use inside the composition root. You can implement DI with no container at all (Pure DI / Poor Man's DI).
- Multiple composition roots in one app (test, prod, dev) are fine -- as long as each run has exactly one in force.
- The composition root does not belong in the domain layer. If
OrderServiceimports fromcompositionRoot.ts, the boundary is broken. - "Configuration as a singleton" is a frequent leak. Config must be read once, at the configuration boundary, and then injected as typed values.
How To Use It
- Locate (or create) the startup function of the application.
- Move every
newthat constructs an external collaborator into that function. - Pass typed config in; do not read
process.envoutside the boundary. - Wire decorators and proxies where the variation should happen -- one line per layer, readable top-to-bottom.
- Provide at least one alternate composition root for tests that uses fakes instead of real infrastructure.
Check Yourself
- Why does the composition root live in the application layer rather than in the domain layer?
- What is the difference between "config as a singleton" and "config injected at the root"?
- When is more than one composition root legitimate in the same repository?
Mini Drill or Application
In an existing codebase, do three things:
- Find every
newthat constructs infrastructure (HTTP client, DB client, queue, file system). Mark each one. - Draft the single
buildApp()function that would replace them. - Write one integration test that boots the app with a test composition root using in-memory fakes and asserts one end-to-end behavior.
Read This Only If Stuck
- Clean Code: Chapter 11 -- Systems / Dependency Injection
- Good Code, Bad Code: Design with Dependency Injection in Mind
- External: Mark Seemann, Composition Root -- the canonical short definition.
- External: Martin Fowler, Inversion of Control Containers and the Dependency Injection pattern -- original vocabulary.