Skip to content

The Shell as the New Monolith

A microfrontend architecture once again has a central application. It loads remotes, provides the outer frame, and acts as the entry point into the product. So the shell must be the new monolith.

That conclusion is understandable. It is still too simplistic.

A shared product needs a place where its parts are composed. The platform has to determine which remote is activated where, how the top-level navigation is assembled, and what happens when part of the application cannot be loaded. Shared platform capabilities such as authentication context, theme, locale, telemetry, or global notifications do not disappear simply because the frontend has been divided into multiple areas of responsibility.

That centrality alone is not an architectural problem.

A shared entry point is not yet a monolith. It becomes one when it creates a shared obligation to change.

The decisive question is therefore not:

How much code lives in the shell?

It is:

Which changes, states, and domain decisions have to pass through the shell?

A shell can be technically substantial and still form a stable platform boundary. Conversely, a small shell with only a few files can become an integration monolith when almost every domain change introduces another special case into it.

A shell does not become a monolith because of its size, but because of the reach of its changes.

The Shell Is a Composition Root, Not the Product Brain

Section titled “The Shell Is a Composition Root, Not the Product Brain”

A composition root is the place where the essential parts of an application are brought together. The shell knows the available building blocks, activates them, and provides the technical conditions they need to run.

It may know:

  • which remotes exist in principle,
  • how they are loaded and mounted,
  • which technical platform contracts are available,
  • which global product context applies,
  • how loading failures are handled,
  • how top-level navigation is assembled.

That knowledge is necessary to turn separately developed parts into a usable product.

The shell should not know:

  • how a remote models its domain state,
  • which entities it owns,
  • which domain operations follow one another internally,
  • which data it has to reload after a change,
  • which stores or components it uses internally,
  • which other remote should react to a domain event.

The boundary can be simplified like this:

Shell as Composition Root
├── activates remotes
├── provides platform contracts
├── manages technical lifecycles
└── knows integration points
Shell as Product Brain
├── knows domain states
├── interprets domain events
├── coordinates product workflows
├── decides how remotes react
└── changes for domain features

The shell may connect remotes. It must not direct them at the domain level.

That does not mean it has to be passive or insignificant. A composition root can configure routing, evaluate remote manifests, project permissions into global navigation, establish technical error boundaries, and provide platform services. Those responsibilities may require a considerable amount of code.

What matters is not the amount of that code, but the kind of knowledge it contains.

If the shell knows integration points and technical lifecycles, it remains a platform. If it knows invoices, plans, orders, and the domain operations that follow from them, it begins to model the product itself.

A sustainable shell knows technical integration points, while a product brain controls domain states and workflows across several remotes.

The previous articles have already examined authentication, theming, global notifications, and technical platform communication individually. Their implementation does not need to be explained again here. What matters is their shared characteristic: they describe the product as a platform, not the domain models of individual remotes.

A central platform capability is therefore not automatically centralized domain logic. Authentication context, theme, locale, global navigation, notifications, telemetry, feature configuration, technical error handling, and the remote lifecycle can all be legitimate responsibilities of a shell or its supporting frontend platform.

The central condition is:

A platform capability remains sustainable as long as it stays domain-neutral and does not need to know the internal models of individual product areas.

The shell may receive a notification intent without understanding which entity was changed beforehand. It may provide an authentication context without deciding whether a concrete domain operation is permitted. It may enable navigation without modelling the business process of a remote.

The shared capability is not the problem. The problem is the domain knowledge that gradually accumulates around it.

A notification queue then becomes a system that interprets domain events. Global navigation turns into a business workflow. Feature configuration becomes a collection of domain-specific exceptions. The authentication context becomes a central authorization mechanism for individual entities.

The original platform responsibility still looks the same from the outside. Its internal meaning has changed.

When Platform Responsibility Turns into Domain Logic

Section titled “When Platform Responsibility Turns into Domain Logic”

The article on global events has already shown that a central channel does not dissolve domain coupling. For the shell, there is an additional effect: it becomes the owner of the reaction graph.

A domain signal such as “planning saved” may initially appear harmless. It becomes critical when the shell derives cross-domain consequences from it:

Planning remote reports “planning saved”
Shell decides:
├── reload school districts
├── update statistics remote
├── increase badge
├── change navigation
└── open success dialog

The shell is no longer merely displaying a message. It knows the domain cause, evaluates its significance, and controls the consequences in other product areas.

Central mediation does not reduce the coupling. It collects it in the host. The planning remote is still connected to statistics, navigation, and other projections. Those dependencies are simply no longer visible where the domain change originates.

The shell becomes an integration monolith when it has to know why a remote did something and which other remote must react to it.

It may display a technical notification intent. It should not decide which domain projections have to be invalidated after a change. It may activate a remote. It should not continue its business process.

When Every Product Change Has to Pass Through the Shell

Section titled “When Every Product Change Has to Pass Through the Shell”

One of the clearest warning signs is a recurring flow like this:

Remote develops a domain feature
Shell needs a new route, event,
state, or special case
Shell release and integration test
Remote can be released

Such a flow may be legitimate for fundamental extensions. A new remote may need a new integration point. A new global platform capability has to be provided somewhere. A deliberately changed product contract may also require a coordinated adjustment.

It becomes problematic when this flow turns into the default for local product development.

An independently deployable remote is not independent if its domain change has to pass through the shell first.

Legitimate reasons to change the shell may include:

  • a new global platform capability,
  • a new level of top-level navigation,
  • a new integration model,
  • a change to a shared product contract,
  • the introduction of an entirely new remote.

Other changes should normally stay within the responsible remote:

  • a new form field,
  • a new domain operation,
  • a changed validation rule,
  • a new internal projection,
  • modified filter logic,
  • a new reaction to a local domain state.

Not every shell change is an architectural mistake. The kind of change that triggers it is the relevant measure.

If the shell changes because the platform changes, it is fulfilling its role. If it changes because a single domain area receives a new validation rule, the responsibility boundary has probably shifted.

Monolithicity Appears in the Reach of Change

Section titled “Monolithicity Appears in the Reach of Change”

Bundle size and lines of code are poor approximations for architectural monolithicity.

A shell can be technically substantial. Routing, remote resolution, telemetry, authentication, error handling, and compatibility layers are not automatically small. As long as those mechanisms remain stable and are not regularly touched by local domain changes, the shell can still form a sustainable platform boundary.

Another shell may consist of only a few files and still couple every product change.

The reach of change appears in at least four dimensions.

Does a local domain change require shell code to be modified?

If a remote displays another field or adds an internal operation, that should not automatically introduce a new host-specific exception. Otherwise, the shell owns knowledge that is closer to the domain than to integration.

Does a change in one remote regularly require the entire integrated application to be tested?

Some integration testing remains useful. It becomes critical when local changes cannot be verified reliably without bringing up the full system because the shell carries domain states and workflows with it.

Does the shell have to be released together with the remote?

A separate bundle provides little value if activation still has to wait for a coordinated host release. The technical separation remains, but the economically relevant independence is lost.

Does the remote team regularly need pull requests, reviews, or approvals from a central shell team for ordinary product work?

The central codebase then also becomes a central decision path. The coupling exists not only in the repository, but in ownership structures and queues.

Monolithicity is not visible only in source code. It appears in shared changes, tests, releases, and decision paths.

A local remote change remains within one team, while a shell-coupled change requires additional host modifications, integration tests, and coordinated releases.

The article on communication between remotes has already described how jointly owned domain state creates horizontal coupling. For the shell, an additional question matters: what happens when it becomes the owner of that state model?

A problematic shell store may look like this:

Shell Store
├── Authentication
├── Theme
├── Planning
├── Orders
├── Invoices
├── Filters
├── Selection
├── Search Results
└── Remote Status

Not every state in the shell is wrong. Authentication status, theme, locale, a global notification queue, and technical loading or error states of remotes can be legitimate platform state. Depending on the product, the representation of global navigation may belong there as well.

Selected invoices, current plans, order items, domain filters, editing drafts, domain entities, and business workflows are different.

The relevant criterion is the meaning of the state, not the store library being used.

If the shell owns domain state from several remotes, it becomes the owner of a shared product model. Actions and selectors turn into global contracts. Changes to one slice affect several teams. Remotes increasingly become views onto centralized state even if their bundles can still be delivered separately.

A global store can turn technically separate remotes into a logically monolithic application.

The decisive boundary therefore does not run between “store” and “no store”. It runs between platform state and domain product state.

Legitimate platform state contains technical product context, while global product state centralizes domain models from several remotes.

Domain Orchestration Does Not Belong in the Host

Section titled “Domain Orchestration Does Not Belong in the Host”

A shell inevitably orchestrates certain flows. What matters is which kind of flow it controls.

Technical orchestration composes the application. The shell may load remotes, manage mounting and unmounting, handle loading failures, assemble global navigation, provide platform context, and show technical fallbacks.

Domain orchestration, by contrast, composes the business process.

The shell should not advance an order after an approval, refresh several domain projections after a plan has been saved, coordinate domain steps across remotes, or know which entity changed in which remote.

Technical orchestration composes the application. Domain orchestration composes the business process.

The distinction matters because both mechanisms can look similar from the outside. In both cases, central code reacts to an event and triggers further steps. Their semantics are fundamentally different.

When a remote is reactivated after a loading failure, the shell is managing a technical lifecycle. When it decides which domain areas must be recalculated after a plan is saved, it is directing a business workflow.

Domain process logic belongs in the responsible domain or in appropriate backend and domain boundaries. It does not automatically belong in the browser shell merely because all visible parts of the product meet there.

Centralized domain coupling does not disappear when its code is moved out of the actual shell file.

A structure like this remains centralized at the domain level:

Shell
└── platform-orchestrator.ts
├── knows invoices
├── knows plans
├── knows orders
└── coordinates remotes

The same applies to a shared package:

shared-platform-state
├── planning slice
├── invoice slice
└── order slice

Splitting the implementation across several files or libraries may organize the code better. It does not automatically change the responsibility boundary.

An integration monolith does not become modular merely because its domain knowledge has been distributed across several shared libraries.

The same questions remain relevant:

Who owns the domain knowledge? Who has to change when it changes? Which teams, tests, and releases are coupled?

A so-called platform API can also be centralized at the domain level. The word “platform” is not an architectural property. If the API contains domain-specific operations and coordinates several product areas, it forms a product center regardless of whether it is implemented as a service, store, library, or host module.

Independent Deployment Without Independent Change

Section titled “Independent Deployment Without Independent Change”

The shell can become a bottleneck even when all remotes are technically loaded dynamically.

Typical warning signs include:

  • Every team regularly needs changes in the shell.
  • Shell reviews block local product work.
  • Many teams jointly own the code, but nobody clearly owns the domain knowledge it contains.
  • Shell releases require broad regression testing.
  • A faulty shell release affects the entire product.
  • Remotes wait for central approval.
  • Several independent changes are bundled into one shared release.

Under those conditions, a dynamic remote architecture loses a major benefit.

Independent deployment is valuable only if independent change and activation remain possible as well.

A team may be able to publish its bundle to a registry at any time. But if the new function becomes usable only after a shell change, central approval, and a coordinated product release, that technical capability has little economic value.

A central shell can be technically stable and still become an organizational bottleneck. Stable platform teams can unintentionally drift into this role. Because their interfaces are visible across the product, putting more integration logic there initially appears sensible. Every individual special case looks manageable. Over time, however, a handoff point emerges that almost every product change has to pass through.

The result is longer queues, broader regression tests, and growing caution around change. Central stability is ultimately purchased through shared slowness.

The phrase thin shell is often interpreted as a demand for as little code as possible. That interpretation is too technical.

A thin shell is not necessarily a shell with only a few files or a particularly small bundle.

Thin Shell
├── little domain knowledge
├── small, stable contracts
├── clear platform ownership
├── low change frequency caused by domain features
└── no cross-domain state coordination

A thin shell has little domain knowledge, not necessarily little code.

It may implement routing, authentication, telemetry, theming, navigation, remote resolution, error boundaries, and technical lifecycles. Those capabilities can be demanding and form a substantial codebase.

The shell still remains thin at the domain level when these responsibilities are cut as neutral platform capabilities and ordinary product changes do not touch them.

Conversely, a small shell can behave monolithically. A few switch blocks, event handlers, or global selectors are enough when every new feature adds another domain-specific exception.

“Thin” therefore does not describe physical size. It describes the limited reach of the shell’s knowledge.

The Shell Knows Contracts, Not Domain Models

Section titled “The Shell Knows Contracts, Not Domain Models”

A sustainable target model does not separate platform and domain through complete isolation, but through deliberately small integration contracts.

Shell / Platform
├── Composition
├── AuthContext
├── ThemeContext
├── Navigation Contract
├── Notification Intent
├── Telemetry
└── technical remote lifecycles
Remote
├── own domain model
├── own state
├── own use cases
├── own API gateways
└── own release

The shell knows the contract, the technical lifecycle, and the product-wide capability.

It does not know a remote’s internal entities, use cases, store structures, concrete data models, or follow-up domain operations.

The shell knows integration points, not the internal domain models of the remotes.

This boundary does not eliminate every dependency. A composed product has shared conventions and contracts. The goal is not complete independence, but controlled dependency in the right places.

Platform contracts may change as well. A platform can provide new capabilities, extend existing contracts, or replace an integration model. Compared with ordinary domain changes, however, such changes should be rare, have clear ownership, and be deliberately versioned. Backward compatibility may be worthwhile when it enables independent migration at an acceptable cost.

A platform contract may evolve. It must not become the daily handoff point for every domain change.

Whether a shell has already become an integration monolith is rarely visible in a single class. A set of concrete questions is more revealing:

  • Can a team develop, test, and release a local domain change without modifying the shell?
  • Does the shell need to understand the domain reason behind a change?
  • Does it combine state from several domain areas?
  • Does it know domain-specific events or entities?
  • Does it decide which remote should react to a domain change?
  • Does a remote change regularly require a shell release?
  • Does a local change require the entire application to be tested?
  • Do nearly all teams regularly work in the shell repository?
  • Does a global store contain domain state from several remotes?
  • Would replacing a remote leave domain-specific exceptions behind in the shell?

A single “yes” does not prove that the architecture is monolithic. Products have real integration requirements, and some changes must be coordinated deliberately.

The more often the answer is “yes”, however, the closer the architecture is to an integration monolith.

The combination of domain knowledge and organizational dependency is particularly revealing. If the shell knows a domain event and changing that event also requires approval from several teams, the coupling is effective both technically and organizationally.

The Platform Connects, the Domain Remains Distributed

Section titled “The Platform Connects, the Domain Remains Distributed”

A shared shell can be valuable. It holds the product together, provides platform capabilities consistently, simplifies technical integration, enables a coherent product experience, and enforces global technical rules in one clear place.

The alternative to a monolithic shell is therefore not automatically having no shell at all.

The better alternative is a domain-neutral platform with clear responsibilities and small contracts.

Centrality is not the problem. Unlimited domain responsibility is.

Such a platform can be operated centrally by design. It can have a clear owner, meet high quality standards, and provide infrastructure that is critical to the entire product. That is precisely why its area of responsibility should be protected.

The more domain knowledge moves into the shell, the more coordination, regression testing, coupled releases, onboarding costs, and conflicts between teams grow. The impact of failure increases. At the same time, fear of change grows because even local adaptations may have unexpected consequences in the shared integration model.

A domain-neutral platform, by contrast, enables local changes, bounded tests, independent activation, clear ownership, and stable integration contracts. Remotes remain more replaceable because their internal models do not live on in the host.

The shell becomes an economic monolith when its central stability can only be maintained through shared slowness.

A sustainable shell composes the application, manages technical lifecycles, and provides shared platform capabilities through small, stable contracts. It knows the integration points of the remotes, but not their internal domain models.

It is a composition root, not the product brain. Its quality is not demonstrated by a small number of files, but by a limited reach of change. Independent deployment remains valuable only when independent change is preserved as well.

The shell may compose the product. It must not become the product itself.