Orchestrating Scalable Frontends: The Power of the Composition Root
Source: Dev.to
In previous chapters we built the foundations of our frontend architecture. With Atomic Design we organized the UI into shared/components, separating reusable, domain‑agnostic elements from specific contexts. With Feature‑Driven Architecture we isolated the domain by introducing features/ as autonomous business units.
But a real‑world application isn’t just a collection of isolated modules. It is a system where domains must collaborate. The question becomes inevitable: How can we allow features to communicate with each other without destroying the isolation we just built?
The answer lies in introducing a higher level of orchestration: the Pages layer acting as a Composition Root.
Revisiting the Structure
Our architecture, consistent with the previous principles, looks like this:
src/
├─ shared/ # Reusable UI & utilities (Atomic Design)
├─ features/ # Isolated domains (Feature‑Driven)
│ ├─ cart/
│ └─ checkout/
├─ pages/ # Orchestration & Composition Layer
└─ app/ # Global bootstrapping
We have already defined a fundamental rule: A feature can depend on shared/, but it must not know about the existence of other features.
features/cart should never import anything from features/checkout. This design choice preserves domain independence.
The Risk of the “Cannibal Feature” (God Feature)
Why can’t we just let features talk to each other? Because refusing a higher orchestration layer exposes us to a fatal risk: Feature Cannibalization.
In a flat architecture, if the Cart needs to trigger the Checkout, a developer will eventually be forced to import the Checkout module into the Cart. At that moment, isolation vanishes. One of the two features “eats” the other, becoming a bloated God Feature that drags along dependencies, logic, and types that do not belong to it.
The Illusion of Simplicity
Removing the pages/ layer might seem like a simplification, but it merely hides complexity. If the Cart feature contains navigation logic to the Checkout, that component is no longer truly reusable. Want to use the Cart in an informational sidebar without a checkout tomorrow? You’d have to drag the entire payment system along.
Principle: A feature must be “dumb” regarding the global context to be “smart” regarding its business logic.
Pages as a Composition Root
In Clean Architecture, Robert C. Martin introduces the concept of the Composition Root—the place where dependencies are assembled and modules are composed. In the frontend, the Page represents this architectural pivot.
The Page is not just a route container; it is the level where:
- Features are instantiated.
- Their contracts (callbacks, events) are connected.
- Responsibilities are coordinated.
The Director and the Actors
Think of the Page as a theater director:
- Features are the actors: they know how to play their part perfectly, but they don’t decide when to enter the stage.
- The Page is the director: it knows that when Actor A (
Cart) emits an event, it must signal Actor B (Checkout) to step in.
Hierarchy of Responsibilities
The architecture takes a clear shape based on decreasing knowledge:
| Layer | Responsibility | Knowledge Level |
|---|---|---|
app/ | Bootstrapping | Knows everything (global state, routing) |
pages/ | Orchestrate | Knows multiple features, but not their internal logic |
features/ | Domain | Knows only itself |
shared/ | Utility | Domain‑agnostic (knows nothing of business) |
The correct flow is always vertical:
Feature A ← Page → Feature B
(Never: Feature A → Feature B)
Substitutability and Maturity
Many codebases start by allowing direct communication between modules. It works while the project is small, but over time changes propagate unpredictably.
Introducing an explicit orchestration layer is the moment a project stops being merely “functional” and becomes truly evolvable. Imagine wanting to replace the checkout/ domain entirely:
features/
├─ cart/
├─ checkout/
└─ new-checkout/
Only the pages/ layer changes; the Cart remains untouched. This is possible because orchestration is centralized in the composition root, not distributed across features.
The Core Principle
- Features solve business problems.
- Pages solve coordination problems.
Atomic Design gave us order in the UI. Feature‑Driven Architecture gave us domain isolation. The Pages layer gives us control over the flows. Separation is important, but knowing where to compose is even more vital.
Next Step: Toward the Page‑Level Store
As long as the data flow is linear, composition via props in the Page is sufficient. But what happens when multiple features must stay synchronized in real‑time or a complex dashboard requires shared state without violating isolation?
In the next article we will explore the Page‑Level Store: an ephemeral, page‑scoped state that allows fluid communication while keeping domain boundaries intact.
Connect on LinkedIn for more updates and insights.