Skip to content

How Do Remotes Communicate? They Don’t.

Remotes do not communicate with one another. They communicate with the sources responsible for their respective domain projections.

This does not mean that no technical signal may ever pass between two remotes. It means that a remote should not subscribe to actions, state transitions, or domain browser events from another area of responsibility. A technically global action stream is not a domain event bus either.

This restriction is not an arbitrary architecture rule. It follows from the domain encapsulation that justifies microfrontends in the first place. Remotes may appear next to one another in the same browser window. That does not turn them into neighboring components inside one shared state model.

Visual proximity is not a communication relationship.

With conventional components, asking how they communicate is entirely reasonable. A parent component passes down state. A child component emits an event. Two neighboring areas coordinate through a shared store.

That model is often transferred unchanged to microfrontends:

Remote A
└── passes state or event
Remote B

This treats remotes as especially large components of the same application. For domain state, that is the wrong starting point.

A more appropriate model is:

Remote A ──▶ authoritative data source
Remote B ──▶ its own authoritative data source

Both remotes may access the same server-side truth. They may also use different projections of the same underlying domain data. What they do not do is synchronize their local state horizontally with one another.

Remotes are not separated so that their internal state models can then be wired back together in the browser. Anyone who does exactly that may separate bundles, deployment artifacts, or repository areas, but immediately recreates the domain coupling.

A microfrontend is not a component inside a magically reactive global application. It is an independently runnable application that is integrated into a host.

That model involves more than a separately built JavaScript bundle. A remote should have its own lifecycle, be runnable and testable on its own, and own its domain state. It may be loaded at a different time from other remotes. It may be updated or rolled back independently. It may appear in several hosts, be delivered from another origin, or use a different framework version.

Not every system must exploit all of these possibilities completely. They nevertheless describe the boundary the architecture should be designed around.

An independent application does not automatically become current merely because another application changed state in the same browser window. After a reload, it does not know the history of actions that were previously dispatched. If it loads later, it did not receive earlier browser events. If it runs in another host or tab, the same JavaScript context may not even exist.

This is not a technical omission that needs to be fixed with an especially clever event bus. It is a consequence of genuine independence.

Remotes do not avoid direct communication because the browser forbids it. They should not need it for domain concerns.

The most obvious form of horizontal coupling is a direct import:

Calendar Remote
imports API or state from the Profile Remote

Perhaps the profile remote exports a service, store, or function for reading current profile data. The calendar remote uses that interface because both applications are running together in the browser anyway.

The profile remote then becomes a runtime dependency of the calendar remote. The latter now depends on the other remote being available, activated, and version-compatible. Local development and isolated tests suddenly require an additional container. A rollback may break expectations. Different loading times matter. Internals of the profile remote effectively become a public API even though they were never designed as a stable contract.

Ownership also becomes unclear. May the profile team change an exported function? Must it know every consuming remote? Who coordinates releases when a new data model must be activated across several areas at once?

A remote is not a runtime library for other remotes.

This does not rule out jointly owned technical libraries. A stable, deliberately published contract is something different from accessing the state or application services of a running remote. Reusable components and shared libraries deserve their own discussion. They do not solve the problem of domain state synchronization.

Coupling through a shared dispatcher is less obvious.

A user changes their name in the profile remote. After the change has been saved successfully, an action is dispatched:

Profile Remote
└── dispatch: profileSaved
Calendar Remote
└── reacts to profileSaved

At first glance, this looks clean. The calendar remote imports no profile service and may not even know any of its components. It merely listens for an action and updates the displayed name.

In reality, it now knows the internal language of the profile remote. It knows its action name, payload, and state transitions. It assumes that a particular technical action is dispatched at exactly the moment when the domain change can be considered complete. It makes assumptions about ordering, delivery, and lifecycle.

If the calendar remote was not loaded when the action was dispatched, it misses the action. After a reload, it is not replayed. A second browser tab does not receive it automatically. An older remote version may interpret the payload differently. Renaming or splitting the action becomes a breaking change. After a rollback, producer and consumer may have incompatible expectations.

Most importantly, the consuming remote mistakes an internal process step for domain truth. profileSaved initially describes what the profile remote did. It does not automatically define which data is valid for the calendar remote, which projection it requires, or whether further server-side processing has already completed.

Actions belong to the internal state and process model of the responsible remote. Even when a shared dispatcher exists technically, actions from other domain areas are not a public integration interface.

A global action stream is not a domain event bus.

Domain Browser Events Do Not Solve the Problem

Section titled “Domain Browser Events Do Not Solve the Problem”

Instead of store actions, teams may introduce neutrally named browser events:

profileUpdated
appointmentChanged
patientSelected
invoiceCancelled

A global event bus initially appears decoupled. Producers and consumers do not know one another directly. In a prototype, a small JavaScript abstraction is often enough to make updates appear immediately on the screen.

As the system lives longer, however, the same questions emerge as in any distributed communication system: What happens to late subscribers? Must events be replayed? What ordering applies? Are they acknowledged or deduplicated? What happens after a reload or in another tab? Does the event contain only an identifier or the complete state? Who owns its schema and versioning? Is it merely a notification or already the domain truth?

As long as an event is only an ephemeral optimization, some of these questions may not matter. Once durable, reliable, ordered, or repeatable delivery becomes necessary, the frontend starts rebuilding a distributed event system.

As soon as browser events need to be stored, acknowledged, replayed, or processed in order, the frontend is creating an unreliable version of an event broker or outbox mechanism.

Domain consistency does not belong in an ephemeral browser event bus.

The Global Domain Store Is a Hidden Frontend Monolith

Section titled “The Global Domain Store Is a Hidden Frontend Monolith”

A third approach appears to avoid direct remote-to-remote communication entirely:

Profile Remote ──────┐
Calendar Remote ─────┼── jointly owned domain store
Billing Remote ──────┘

All remotes read and write one central domain state. Changes become visible immediately. Additional requests seem unnecessary. Integration appears simple.

The horizontal coupling has not disappeared. It has merely moved into a shared state container.

The remotes now share data models, actions, selectors, and freshness rules. Several teams modify the same domain truth. A new version of the profile remote may write a store shape that an older calendar remote does not understand. Rollbacks become harder because state and code can no longer be considered independently. Tests require global state transitions. Starting a single remote depends on the host or other remotes constructing the store correctly.

When remotes work only together with a shared domain store, the bundles have been separated, but the application has not.

The problem becomes even more pronounced across frameworks. Angular, React, and Vue remotes can technically access the same store through a shared JavaScript abstraction. But that does not define how reactivity, subscriptions, lifecycles, error handling, and compatibility will work reliably together.

What looked like neutral infrastructure quickly becomes a custom cross-framework state-management framework. In addition to their actual applications, the teams now operate a shared runtime contract that every remote must understand.

Horizontally coupled remotes share actions, events, and domain state. Independent remotes obtain their own projections from authoritative sources.

Criticism of jointly owned domain state is not a blanket criticism of Redux, NgRx, or a technically global store instance.

Traditional applications often provided one central root store:

Root Store
├── Calendar Feature State
├── Profile Feature State
└── Billing Feature State

The registration was technically global. The domain areas could still remain largely encapsulated. A feature owned its own reducers, actions, selectors, effects, and state model. Other features did not need to subscribe to or modify those internals.

In microfrontend systems, a store could also be provided inside the container of the individual remote:

Host
└── Calendar Remote
└── own Store
└── Calendar State

State and lifecycle therefore remained inside the remote. The host did not have to become responsible for the domain model.

The actual antipattern is not a global store instance. The antipattern is jointly owned domain state shared by independent remotes.

A remote may of course use Redux, NgRx, a signal store, or any other state-management solution internally. It becomes problematic when several remotes read and modify the same domain state, use actions or selectors from other domain areas as integration contracts, or can no longer function independently without the host’s global state.

The growing adoption of locally provided stores makes this boundary easier to see. Modern store approaches can live closer to a feature or remote and therefore have a clearer lifecycle. That does not invalidate traditional global store infrastructure. It merely reminds us that technical state does not automatically need to be shared at application level.

Domain state should live where it is owned.

A Name Changes—What Happens in the Calendar?

Section titled “A Name Changes—What Happens in the Calendar?”

A concrete example makes the distinction clearer.

A user updates their name in the profile remote:

Profile Remote
└── writes change to the backend

Elsewhere, a calendar remote displays that user’s name. The immediate reaction is often: after saving, the profile remote must pass the new name to the calendar remote.

The obvious solutions are an action or a shared profile object:

Profile Remote
└── profileSaved ──▶ Calendar Remote

or:

Profile Remote
└── writes profile object to global store
Calendar Remote

Both approaches assume that the calendar remote adopts the state model of the profile remote. It may not need a complete profile at all.

The profile remote works with names, contact details, settings, and other editable profile data. The calendar remote may need only a display name, initials, and availability. A billing remote may care about the billing address and payer. A document remote may use only a legally formatted name.

These models may be based on the same underlying data. They are still not identical.

Profile Remote
└── complete profile data
Calendar Remote
└── display name, initials, and availability
Billing Remote
└── billing address and payer

A remote is therefore not simply loading the same data again. It is loading the projection that fits its own use case.

A more robust model is:

Profile Remote ── writes ──▶ authoritative source
Calendar Remote ── reads when needed ──▶ own projection

The writing remote owns the user intent. The authoritative server-side source owns the domain truth. Other remotes own the projections required for their own use cases.

No remote needs to know the internal actions, store schemas, or state transitions of another remote.

How Does the Calendar Remote Stay Current?

Section titled “How Does the Calendar Remote Stay Current?”

The calendar remote does not need to learn immediately that a particular save operation happened inside the profile remote. It only needs to recognize when its own projection may no longer be current.

Several strategies are available. A projection can be loaded when entering an area and revalidated after a domain-appropriate period. Returning focus may trigger a check. Individual records may be refreshed selectively. HTTP caches, query caches, and local stores can avoid unnecessary transfers. Depending on the use case, polling, Server-Sent Events, or WebSockets may be appropriate.

The specific mechanism is less important than the direction of the dependency.

A server-side signal could, for example, announce:

User data for user 4711 has changed.

The calendar remote can then invalidate its own projection for user 4711 and reload it when necessary. The signal does not transport the complete profile model and does not write foreign state into the calendar store.

Invalidate a projection instead of synchronizing domain state between remotes.

The distinction matters: the calendar remote is not reacting to an internal operation of the profile remote. It is reacting to the possibility that its own data has become stale. It reads the new domain truth through its own backend, BFF, or integration contract.

Profile Remote
└── writes change
Backend / authoritative source
├── persists domain truth
└── optional invalidation signal
Calendar Remote
└── reloads own projection

Losing an invalidation signal must not mean losing domain truth. The remote therefore also needs a robust revalidation strategy. The signal is a hint that data may be stale, not a replacement for the authoritative source.

A profile remote writes a change, the backend persists the domain truth, a signal invalidates a projection, and the calendar remote rereads its own projection.

Does Every Remote Now Have to Reload Data Constantly?

Section titled “Does Every Remote Now Have to Reload Data Constantly?”

The performance concern is legitimate. When each remote manages its own projection, additional requests may occur. Network cost and latency do not disappear merely because the domain boundary is well designed.

Independence does not mean reloading all data after every interaction. It means that a remote owns its data needs and freshness rules.

The calendar remote knows which data it needs and how long its projection can remain sufficiently current for the use case. It can revalidate a single record rather than rebuilding the entire calendar. It can cache responses locally or through HTTP. It can coalesce equivalent requests, use server-side aggregated projections, or defer refreshes until the next domain-relevant interaction.

A remote-specific projection is often smaller than a global model. The calendar remote does not need the full profile object merely to display a name. Conversely, it does not have to follow the same freshness rules that apply when contact details are edited in the profile.

A global profile object initially looks efficient because it is loaded only once. In reality, it couples every consumer to the same schema, version, and freshness rules. A change for one use case can then affect every remote.

Performance optimization belongs to the individual remote and its backend contract—it is not a reason to share domain state horizontally between remotes.

Redundant requests are a performance problem. Jointly owned domain state is an architecture problem. One should not be solved hastily by introducing the other.

An internal feature of a conventional application can often assume that a global store already contains the current state. A microfrontend should not adopt that assumption unnoticed.

It may run on its own, appear in another host, load later, or use a different version. After a reload, it reconstructs its state. It knows nothing about actions that another remote dispatched before it started.

An independently runnable application therefore does not receive domain updates by magic. It needs its own revalidation strategy; an invalidation signal may complement that strategy, but it cannot replace it.

This is not a weakness of the architecture. It is a consequence of genuine encapsulation.

A system that does not want to accept this consequence may not need independent microfrontends. Modularized features inside one jointly delivered application may be entirely appropriate. The problem begins only when the architecture promises independent remotes while their ability to function still depends on one shared domain runtime model.

Technical Platform Coordination Is Still Possible

Section titled “Technical Platform Coordination Is Still Possible”

Not every global signal carries domain truth.

A host may expose a small, stable contract for technical platform coordination. Depending on the system, this may include navigation, logout, language, theme, or a remote failing to load:

navigateTo
logout
localeChanged
themeChanged
remoteLoadFailed

Not every example must necessarily be implemented as an event. The category is what matters: these signals coordinate the platform or user interface. They do not publish the domain state of a profile, calendar, or billing remote.

Platform context may be shared. Domain truth does not belong in a global browser store.

Where exactly the boundary for global events lies and which contracts a host should expose is a separate topic. For the communication of domain state, the distinction is sufficient: technical platform coordination is not the same as horizontal state synchronization.

A shared store or browser event bus often looks convincing in the first prototype. The change becomes visible immediately. One additional request disappears. The technical effort remains low, and the demo works.

The costs appear later.

Versions can no longer be activated independently. Rollbacks must account for the expectations of unknown consumers. Tests require several remotes and global state transitions. Failures depend on loading time, ordering, or browser tab. Teams modify shared models whose actual owners are no longer clear. Local development becomes harder because a remote does not work fully without the host, event bus, or global store.

The request saved in the prototype may later be paid for through shared ownership and coupled releases.

The alternative may introduce additional reads and revalidation. In return, contracts remain explicit. Each remote controls its state, lifecycle, and freshness rules. Changes to the profile model do not automatically require changes in the calendar. A calendar remote can be rolled back or tested separately without reproducing the internal processes of the profile remote.

This separation is not free. It moves the work to the right place: explicit projections, owned backend contracts, and domain-appropriate freshness rules.

The argument leads to strict but simple consequences.

A remote does not subscribe to domain actions from another area of responsibility. Domain browser events are not used as a horizontal state channel. Remotes do not share jointly owned domain state through a global store. A remote does not import the runtime services or store internals of another remote.

Instead, every remote owns its own projection and reads it through its own contract. An optional signal may indicate that the projection could be stale. The new domain truth still comes from the source responsible for it.

This does not mean that every remote needs its own backend. Frontend and backend decomposition remain separate decisions. Several remotes may use the same API, a shared BFF, or a frontend-facing integration layer. What matters is that their contracts and projections do not emerge from the internal state models of other remotes.

Remotes do not need to know the internal change process of another remote. They need to be able to recognize when their own projection is no longer reliable.

Treat remotes as separate applications. Then the question of how they communicate becomes the right question again: Who owns the state?

Remotes do not communicate with one another. They communicate with the sources responsible for their respective domain projections.