PATTERN Cited by 1 source
Request-state-propagated presentation context¶
Problem¶
In a micro-frontend architecture, many independently-owned renderers each query a shared GraphQL BFF for their slice of the page. Sometimes the whole page needs a single coherent presentation context — a theme, a feature-flag bundle, a visibility policy — derived from the customer's intent at the page level (not from any individual renderer's data).
The anti-patterns:
- Each renderer re-derives context. Every micro-frontend duplicates the intent-parsing logic. Ten renderers, ten chances for the brand-code matcher to drift out of sync. If the page turns out to have contradictory context (one renderer decides "elevated", another "default"), the page looks broken.
- Context travels as a side channel outside the query. E.g. a header on every backend call. The BFF has to remember to propagate, backends have to remember to read; any middleware between them can drop it silently.
- Upstream pre-computes all policies and bakes them into every query. Makes every query carry the whole policy dictionary, amplifying request size and coupling the aggregator to the policy vocabulary.
Solution¶
Resolve the presentation context once at the root-entity step in the aggregator, store it by name in the aggregator's request state, and have every downstream fan-out re-query the aggregator with the resolved context as an explicit parameter.
Zalando's canonical instance of this pattern is the Experience mechanism in the Interface Framework — see concepts/root-entity-experience-resolution for the resolution step, concepts/presentation-policy for what the context carries.
Rendering Engine FSA (GraphQL aggregator)
│ │
│ 1. root entity resolution + experience │
│─────────────────────────────────────────▶│── → Catalog backend
│ │ picks Experience
│ ◀───── (root entity, experience=XP) ────│
│ │
[stash experience=XP in request state] │
│ │
│ 2. child renderer N fans out │
│─── query(entity-M, experience=XP) ───────▶│── → Product backend
│ │ applies XP policies
│ ◀───── data shaped by XP's policies ────│
│ │
... ...
(Source: sources/2023-06-25-zalando-context-based-experience-in-zalando.)
Invariants¶
- Single-resolution. The context is picked exactly once per request. Re-resolving it in downstream calls is forbidden.
- Named, not inlined. The aggregator request state holds a name for the context (e.g. the Experience id), not the resolved policy dictionary. Policy lookup happens in the domain backend that owns the data. This keeps the request state tiny and avoids duplicating the policy dictionary on every backend call.
- Explicit parameter in child queries. The child renderer's query to the aggregator carries the context name as a first- class argument. Relying on an ambient header that flows in band but is not part of the query API would make it invisible to persisted-query caches and GraphQL schema tooling.
- Resolution happens at a step that already exists. Zalando piggybacks on root-entity resolution (a step that had to happen anyway to know what page to build). Adding a separate pre-flight "context resolution" phase is avoided.
Where it goes beyond a classic theme provider¶
A React-style theme provider lets child components read theme values via context, but requires each component to know about the theme. This pattern is broader in two ways:
- The context is resolved server-side by the backend closest to the customer's intent (search / catalog / product backend), not by the frontend.
- It travels across service boundaries, not within a single React tree. Each micro-frontend goes through the aggregator to reach its domain backend; the context travels on each GraphQL call, not via React context.
Trade-offs¶
Pros:
- One intent-parsing code path per surface (owned by the domain backend closest to that surface).
- Domain backends stay stateless w.r.t. intent: they receive an Experience name, apply its policies, return data.
- Request state is minimal (one name), so the aggregator isn't carrying a large policy dictionary.
Cons / constraints:
- Requires a designated root step where resolution can happen before the fan-out. If the aggregator can't identify a single pre-fan-out moment, this pattern doesn't fit.
- Conflict resolution is non-trivial. When multiple contexts match, see patterns/fallback-rule-for-experience-conflict.
- Must propagate through every layer; skipping one means the affected renderer silently uses the default context without an error.
- Depends on every domain backend knowing the context name's vocabulary; evolving it (adding a new Experience) requires coordination with whichever backends care.
When to use¶
- Micro-frontend or federated-BFF architecture where many renderers call a shared aggregator.
- Whole-page presentation context that varies per request (theme, visibility flags, branded elevation, A/B bucket applied consistently).
- A natural root step in the request lifecycle where resolution fits (e.g. root-entity resolution, session establishment, landing page load).
When not to use¶
- Context is per-component, not per-page (just use React context / equivalent).
- Context is per-user-session and doesn't change by request (keep it in session state / JWT claims).
- Backends are already stateful about the customer's intent and would ignore an incoming context parameter.
Seen in¶
- sources/2023-06-25-zalando-context-based-experience-in-zalando — canonical Zalando IF implementation using Experience as the request-state-propagated presentation context; two-step resolve-then-propagate flow with the Rendering Engine storing the Experience name and FSA passing it to every downstream backend.
Related¶
- concepts/experience-zalando-if
- concepts/root-entity-experience-resolution
- concepts/customer-intent-experience-selection
- concepts/presentation-policy
- concepts/micro-frontends
- systems/zalando-rendering-engine
- systems/zalando-fashion-store-api
- patterns/entity-to-renderer-mapping
- patterns/fallback-rule-for-experience-conflict