DEV Community

Cover image for Orchestrating Scalable Frontends: The Power of the Composition Root
Giuseppe Ciullo
Giuseppe Ciullo

Posted on

Orchestrating Scalable Frontends: The Power of the Composition Root

In previous chapters, we built the foundations of our frontend architecture. With Atomic Design, we organized our 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 our 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

Enter fullscreen mode Exit fullscreen mode

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 is not a technical limitation; it is a design choice that 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 module—a God Feature—that drags along dependencies, logic, and types that do not belong to it.

The Illusion of Simplicity

Eliminating the pages/ layer might seem like a simplification. In reality, we are just hiding complexity. If the Cart feature contains the navigation logic to the Checkout, that component is no longer truly reusable. If you wanted to use that Cart in an informational Sidebar without a checkout tomorrow, you couldn't do it without dragging along the entire payment system.

The principle is clear: A Feature must be "dumb" regarding the global context to be "smart" regarding its business logic.


Pages as a Composition Root

In the book Clean Architecture, Robert C. Martin introduces the concept of the Composition Root. It is 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:

  1. Features are instantiated.
  2. Their contracts (callbacks, events) are connected.
  3. 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. Over time, changes propagate unpredictably.

Introducing an explicit orchestration layer is the moment a project stops being just "functional" and becomes truly evolvable. Imagine wanting to replace the checkout/ domain entirely:

features/
├─ cart/
├─ checkout/
└─ new-checkout/

Enter fullscreen mode Exit fullscreen mode

The only change occurs in the pages/ layer. 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.

Top comments (0)