Back to articles
Frontend Architecture

The Art of Resilient Frontends, Part 1: The Mental Model

A mental model for designing frontends that remain useful when dependencies fail, change, or slow down.

In this essay 6 sections
  1. Two bridges
  2. The browser is a node in a distributed system
  3. The distributed-computing fallacies, in the browser
  4. Babelfish: a translation layer
  5. Resilience is built in layers
  6. ECORE: a habit of questioning assumptions

A resilient frontend is not one that never fails. That is impossible. Networks drop. APIs change shape. Users leave tabs open for three days. Phones run out of memory. Third-party scripts change. Every so often, a deployment ships a regression.

A resilient frontend keeps delivering useful behavior when parts of the system fail, change, or slow down.

Sometimes useful behavior means continuing with reduced functionality. Sometimes it means preserving the user’s work. In a high-risk operation, it may simply mean communicating uncertainty and offering a safe next step. Resilience is not the ability to pretend that nothing went wrong. It is the ability to respond without turning one failure into a larger one.

This series uses five recurring disciplines to reason about that response:

The ECORE resilience model shown as a continuous cycle: Expect with a storm cloud, Contain with a shield, Observe with an eye and radar, Recover with a bridge, and Evolve with an upward growth arrow.

ECORE is a reinforcing cycle: expect failure, contain its effects, observe what happened, recover safely, and evolve the system.

Together, they form ECORE.

ECORE is not a strict sequence that begins after an incident. We design containment and observability before failure. Recovery can begin while a failure is still being diagnosed. What we learn changes what we expect next time. The five disciplines overlap and reinforce one another.

This first part establishes the mental model behind them.

Two bridges

Picture two bridges.

Both are designed to handle normal traffic. The first is optimized around those expected conditions. The second is designed for a wider range of stress. It has drainage for heavy rain, expansion joints for temperature changes, barriers that prevent an accident in one lane from spreading, sensors that reveal structural problems, and alternate routes when part of the bridge is unavailable.

On a calm day, both bridges may look equally capable. Their differences become visible under stress because stress exposes the assumptions built into their designs.

Frontend architecture works the same way.

A fragile frontend quietly assumes that:

  • The API will respond.
  • The response will have the expected shape.
  • The JavaScript will load.
  • The user has a fast and stable connection.
  • Every deployed asset belongs to the same version.
  • Third-party code will behave.

A resilient frontend makes those assumptions explicit and asks:

If this assumption fails, what should the product do?

The bridge also gives us the beginnings of ECORE. Designing for storms means we expect stress. Lane barriers contain damage. Sensors help us observe it. Alternate routes let people recover. Inspections and repairs help the system evolve.

The analogy is not the architecture, but it reveals the principle behind it: resilience is designed into a system through multiple independent protections.

A technical blueprint comparing a fragile bridge under storm stress with a resilient bridge protected by drainage, lane barriers, sensors, and an alternate route.

Under normal conditions, both bridges work. Stress reveals the assumptions built into each design.

The browser is a node in a distributed system

Here is the mental shift at the center of this series:

The browser is a node in a distributed system that happens to have a screen attached.

The frontend does not merely “call an API” and display the result. That convenient description hides most of the uncertainty.

The browser often holds a local representation of state owned elsewhere. It exchanges messages with remote services over an unreliable network. Those messages can be delayed, duplicated, rejected, or returned out of order. The local representation can become stale. The user can change it while the server is changing it too. Meanwhile, the page may be running code from a different deployment than the service it is calling.

Not all frontend state is a remote replica. A form draft may belong to the browser. The URL may own navigation state. An offline-first application may accept local writes. The important question is not whether the server is always the source of truth. It is whether ownership is explicit and the system knows how different copies are reconciled.

Once you see the browser as a participant in a distributed system, many frontend failures stop looking like isolated UI bugs. They become familiar problems with established vocabulary.

The distributed-computing fallacies, in the browser

Peter Deutsch’s fallacies of distributed computing describe assumptions that developers repeatedly make about networked systems. They were not written specifically for frontends, but they provide a useful lens for examining browser applications.

False assumptionFrontend realityPossible design response
The network is reliableA connection can disappear during a request, and an unknown response does not prove that a mutation failedTimeouts, bounded retries, idempotent operations, honest uncertain states
Latency is zeroLatency varies by user, device, location, and momentExplicit loading states, prefetching, caching, and optimistic UI where it is safe
Bandwidth is infiniteLarge bundles and payloads increase startup time and impose unequal costsCode splitting, image and bundle budgets, pagination, response shaping
The network is secureTransport and remote data cross trust boundariesTLS, browser security controls, output safety, and runtime validation for untrusted data
Topology does not changeA user can switch networks or reach a different service instance during a flowResume from application state and treat connectivity indicators only as hints
There is one administratorBrowsers, CDNs, ISPs, proxies, extensions, and blockers are controlled independentlyAvoid fragile assumptions about the path and degrade when optional resources are blocked
Transport cost is zeroTransfer, parsing, allocation, and rendering all consume time and memoryReduce payloads, paginate or stream where useful, move heavy computation off the main thread when justified
The network is homogeneousDevices, browsers, networks, and user constraints vary widelyTest representative lower-bound conditions based on the product’s audience

These are not one-to-one translations. A browser has different constraints from a backend service. It is short-lived, resource-constrained, user-controlled, and often has only a local view of system health. The point is not to copy backend patterns mechanically. It is to recognize the underlying failure class and adapt the relevant idea.

Babelfish: a translation layer

When a frontend problem feels unfamiliar, finding its distributed-systems name gives you better vocabulary for investigating it.

Replica and cache coherence

Frontend manifestation
Client query cache plus revalidation
Failure it helps address
Stale data and redundant requests

Explicit state ownership

Frontend manifestation
Server-owned domain state, client-owned drafts, URL-owned navigation state
Failure it helps address
Divergent copies and unclear authority

Eventual consistency

Frontend manifestation
Showing cached data while a fresh version is fetched
Failure it helps address
Blocking every read on the latest remote state

Read-your-writes experience

Frontend manifestation
Optimistic updates followed by authoritative reconciliation
Failure it helps address
A user’s action appearing to disappear

Idempotency

Frontend manifestation
Stable operation identifiers supported by the server
Failure it helps address
Duplicate mutations after retries or lost responses

Backpressure

Frontend manifestation
Debouncing, concurrency limits, and discarding obsolete client work
Failure it helps address
Request floods and main-thread overload

Bulkhead

Frontend manifestation
Isolated widgets and carefully placed render error boundaries
Failure it helps address
One failing area taking down the whole experience

Timeout and jittered retry

Frontend manifestation
Bounded retry policy for classified transient failures
Failure it helps address
Hung requests and synchronized retry storms

Request coalescing

Frontend manifestation
Multiple consumers sharing one in-flight request
Failure it helps address
Many components fetching the same resource

Ordering and causality

Frontend manifestation
Ignoring or cancelling stale responses
Failure it helps address
An older response overwriting newer state

Clock skew

Frontend manifestation
Treating the client clock as untrusted for authoritative decisions
Failure it helps address
Expiration and ordering bugs caused by incorrect local time

Expand-and-contract change

Frontend manifestation
Backward-compatible API and asset deployments
Failure it helps address
Old tabs communicating with newer services

Some mappings are close. Others are analogies. Several, especially idempotency and consistency guarantees, require backend cooperation. Disabling a submit button can reduce accidental duplicates, but it cannot make an operation idempotent across retries, refreshes, or multiple tabs. An optimistic update can provide immediate feedback, but it cannot guarantee that a later authoritative read will include the write.

That distinction matters. Borrow the problem-solving vocabulary, not just the pattern name.

Resilience is built in layers

No single mechanism protects the whole system. Think again about independent safety systems. Drainage does not replace lane barriers. Barriers do not replace sensors. Each addresses a different failure class, and together they reduce the chance that one problem becomes a catastrophe.

Frontend resilience works across several layers:

Frontend resilience shown as four complementary layers: Experience, Application, Data and Network, and Runtime and Delivery. A vertical observability spine crosses every layer and represents errors, metrics, traces, and user journeys.

Resilience comes from complementary protections across the stack, with observability supplying evidence from every layer.

The layers are not a request pipeline. They are complementary places to put defenses.

  • A render error boundary can contain certain failures in a component subtree. It cannot detect a malformed API response or repair a failed mutation.
  • A retry can help with a classified transient network failure. It cannot fix invalid data and may make an outage worse if it is unbounded.
  • TypeScript catches a useful class of development-time inconsistencies. It cannot prove that remote data matches the declared type at runtime.
  • Runtime validation can detect unexpected data. It still needs a product decision about whether to reject, degrade, or recover.
  • A feature flag can limit exposure to a risky change. It needs ownership, monitoring, and a removal plan to remain a safety mechanism.

A system does not need every possible resilience pattern. It needs complementary protections chosen for its actual failure modes and critical user journeys.

ECORE: a habit of questioning assumptions

We can now return to the five disciplines:

DisciplineQuestion
ExpectWhat can fail, slow down, change, or become uncertain?
ContainHow do we prevent that failure from spreading?
ObserveWhat evidence will tell us what the user experienced and why?
RecoverWhat safe path remains for the user and the system?
EvolveWhat should change in the product, architecture, tests, or ownership model afterward?

ECORE is deliberately simple. It does not replace threat modeling, performance analysis, incident response, or good product design. It gives us a common set of questions to apply across all of them.

Before moving to Part 2, try it on one critical journey in your product. Pick something concrete, such as signing in, publishing a document, submitting an order, or loading a dashboard. List every dependency the journey crosses. Then ask what the user sees if each dependency is slow, unavailable, stale, or returns an unexpected result.

Do not solve every failure yet. First, make the hidden assumptions visible.

That is where resilient design begins.

In Part 2, we will move from expecting failure to containing it. We will examine how boundaries, explicit UI states, runtime validation, partial success, and graceful degradation keep one broken dependency from taking down the entire experience.

Join the discussion

Thoughts, questions, or a different perspective?

React to this essay or continue the conversation. Comments are powered by GitHub Discussions.