TECH · PART 7
Wiring It Together
Part 7 of 8: The composition root as the big bang — constructing every service once into a frozen, immutable tree
On this page
We have the architecture: three layers, mappers, pure domain. Now we need to actually build the thing. This part shows how services are constructed, how dependencies flow, and how the entire application comes together at a single point — the composition root.
Services as Stateless Objects
In Part 5, we established that there are two kinds of things: services (behavior) and structures (data). Now we need to talk about how services are actually built.
A service is a class with two kinds of members:
- Injected collaborators — interfaces to other services, repositories, clients
- Configuration — immutable values like timeouts, endpoints, feature flags
Both are set in the constructor and never changed. The service holds no mutable state.
class OrderService:
private readonly repo: IOrderRepository
private readonly pricing: IPricingService
private readonly logger: ILogger
private readonly maxItems: int
constructor(repo: IOrderRepository, pricing: IPricingService, logger: ILogger, maxItems: int):
this.repo = repo
this.pricing = pricing
this.logger = logger
this.maxItems = maxItems
create(record: OrderRecord): Result<Order, OrderError>:
if record.items.length > this.maxItems:
return Err(OrderError.TooManyItems)
// ... All state flows through method parameters and return values. The instance itself is frozen at construction.
This is the synthesis of OO structure and functional thinking from Part 4: services are objects for organization, but their methods behave like pure functions that happen to have access to injected collaborators. You get the navigability and discoverability of OOP with the predictability of functional code.
The Big Bang
Think about the universe. At the very beginning, there was a single moment — the big bang — where all the fundamental forces and particles came into existence. After that initial moment, the universe just... runs. Particles interact, stars form, galaxies emerge. The laws of physics do not change after the big bang; they were all established in that first instant.
The composition root is your application's big bang.
It is a single point — main(), the
application bootstrap, a DI container configuration — where
every service in the system is created, wired together, and frozen. After this moment, no new
services are created. No dependencies change. The structure of the universe is set.
Cooling down…
The composition root as a big bang: every service is constructed once — infrastructure, then repositories, domain services, controllers, and finally the App — into a dependency tree that freezes into immutable laws. Afterward, events (matter) flow down through the frozen tree and results flow back up.
function main():
// Layer 0: Infrastructure — the raw materials
db = new PostgresDatabase(config.connectionString)
httpClient = new HttpClientAdapter()
logger = new ConsoleLogger()
// Layer 1: Repositories — the outward layer takes shape
userRepo = new UserRepository(db)
orderRepo = new OrderRepository(db)
// Layer 2: Domain services — the core comes to life
userService = new UserService(userRepo, logger)
orderService = new OrderService(orderRepo, userService, logger, config.maxItems)
// Layer 3: Controllers — the inward layer connects to the outside
userController = new UserController(userService)
orderController = new OrderController(orderService)
// Ignition — the universe starts running
app = new Application(userController, orderController)
app.run()
Read it top to bottom. Infrastructure first, then repositories, then services, then controllers. Each layer receives
its dependencies from the layers above it. And then, at the very end,
app.run() — the moment the universe starts.
After that, events drive everything. An HTTP request arrives. The framework routes it to a controller. The controller calls a domain service. The service calls a repository. Data flows through the tree of services that was established at the big bang. Nothing is created on the fly. Nothing is resolved lazily. The entire structure was determined at startup.
This is powerful because it means the application's behavior is predictable. There are no surprises hidden in factory methods or service locators. No "oh, this service gets created the first time someone calls it, and depending on when that happens, it might get a different configuration." The entire dependency tree is visible, explicit, and frozen.
Why a Single Point
The composition root is the only place that knows about concrete types. Everything else depends on interfaces. This gives you:
- Visibility — the dependency graph is readable in one place
- No hidden wiring — no
newcalls scattered through the codebase - Easy swapping — changing an implementation means changing one line here
- Testability — tests can replace any layer by constructing a different tree
Trees, Not Graphs
With this approach, the dependency graph is a tree rooted at the entry point. Each service receives its dependencies once, at construction, and those references never change. No lazy initialization. No conditional resolution. No ambient context.
Compare this to traditional OOP where object A creates object B inside a method, B grabs C from a static factory, C
reads config from a global singleton. The dependency graph becomes a tangled web that you discover only by stepping
through debuggers. Here, the entire tree is visible in
main().
Laws and Matter
The physics analogy goes deeper than just the big bang moment. Think about what the universe actually contains after that initial instant.
There are laws — gravity, electromagnetism, the strong and weak nuclear forces. These were established at the big bang and have not changed since. They are immutable. They govern how everything interacts — how particles attract and repel, how energy transfers, how matter behaves — but the laws themselves never change.
And there is matter and energy — particles, atoms, molecules, radiation. These flow through the universe, constantly changing form. Hydrogen fuses into helium inside stars. Energy radiates outward as light. Molecules combine into complex structures and break apart again. Matter and energy are never created from nothing and never destroyed into nothing. They transform, rearrange, and recombine into endlessly new configurations — all governed by the unchanging laws.
Our system works the same way.
Services are the laws. Established at the composition root, immutable thereafter. They define
how things interact — validation rules, business logic, orchestration patterns, data transformations. A
PostService always validates titles the
same way. An OrderService always enforces
the item limit the same way. They do not change during the lifetime of the application. They do not acquire new
dependencies. They do not mutate their configuration. They simply are, and they govern.
Data structures are the matter. They flow through the system, constantly being transformed. An HTTP request body enters the system as raw JSON. The controller mapper shapes it into a domain Record. The service validates it, enriches it, runs it through business rules. The repository mapper reshapes it into a data model. It lands in a database as a row. At no point did a service conjure this data from nothing — it arrived from the outside world and was transformed at every boundary, governed by the services it passed through. And when data leaves (as an API response, a notification, a log entry), it is the same information, reshaped once more for its destination.
After the big bang, the laws are set. Then events introduce matter into the system — an HTTP request arrives, a message appears on a queue, a cron job fires. That data flows through the tree of services, being shaped and transformed at each node, until it reaches its final form. New requests bring new data. The services process it the same way every time, because the laws do not change. The complexity of the system comes not from the services themselves — each one is simple — but from their composition, the way data flows through many simple transformations to produce complex outcomes. Just as the staggering complexity of the physical universe emerges from a handful of unchanging laws applied to matter over time.
Manual vs. Container
Some languages provide DI containers that automate wiring. You register interfaces and implementations, and the container resolves the dependency graph for you.
This is fine. The principle is the same: construct everything once, up front, at the root. The container is a more declarative way to express the same tree.
What matters is that the graph is static, visible, and assembled before business logic runs. Whether
you write new calls yourself or let a
container do it is a stylistic choice. Some teams prefer the explicitness of manual wiring (you can read the
dependency tree like a story). Others prefer the convenience of containers (less boilerplate, automatic lifetime
management). Both work.
The one thing to watch out for with containers is magic. If the container silently resolves dependencies through reflection and you cannot tell, by reading code, what gets injected where — you have traded explicit wiring for implicit wiring, which undermines the whole point. A good container configuration should read almost as clearly as manual wiring.
Quick Checklist
| Concern | Check |
|---|---|
| Service members | Only readonly members (injected collaborators + config) |
| Mutable state | No mutable instance state on any service |
| Composition root | All services constructed at a single point |
| Dependency graph | A tree, visible in one place |
| Initialization | No lazy initialization or conditional resolution |
| After the big bang | Events drive behavior; the service tree is frozen |
| Framework isolation | Framework types isolated behind interfaces |
What Comes Next
The machine is built. All layers wired. The composition root assembles everything and the engine runs. Events flow in, data flows through, results flow out.
Now the question: how do you know it works?
Part 8: Testing and Testability shows how the architecture we have built makes testing straightforward. The properties that make code testable are the same properties that make it changeable. This entire series has been building toward code that is easy to test — not as an afterthought, but as the proof that the design works.