Skip to content

PATTERN Cited by 1 source

Entity-to-Renderer mapping

Problem

In an entity-based page-composition system, each request produces a tree of typed Entities (e.g. Product, Collection, Outfit) chosen by personalisation. The server has to turn that tree into actual HTML. The same Entity type can be shown many ways — an Outfit can be a main detail view, a card inside a Collection carousel, or a hero image — so there is no one-to-one type→component mapping baked into the Entity.

The question the server has to answer per placement is: which visualisation should this Entity get here?

Solution

Maintain an explicit, declarative set of rendering rules that maps (Entity type, placement context) → Renderer. The runtime — in Zalando's case, the Rendering Engine — walks the Entity tree, applies the rendering rules, and produces a matching tree of Renderer invocations (Source: sources/2021-03-10-zalando-micro-frontends-from-fragments-to-renderers-part-1).

Entity tree (from personalization)         Renderer tree (after rules)

  ProductPage                                ProductPageRenderer
   ├─ Product: A                     →        ├─ ProductMainRenderer(A)
   ├─ Collection: C1                 →        ├─ CollectionCarouselRenderer(C1)
   ├─ Outfit: O1                     →        ├─ OutfitCardRenderer(O1)
   └─ Collection: C2                 →        └─ CollectionCarouselRenderer(C2)

Key invariants

  1. Each Renderer binds to one Entity type. Multi-type Renderers break the tree-walk invariant and push conditional rendering into Renderer code.
  2. The Entity → Renderer relationship is one-to-many. The rules express which of the possible Renderers for this Entity type to pick here — not "does this Entity have a Renderer". Picking is typically driven by placement (top-of-page vs slot-below-product) or context flags from the Entity tree.
  3. Rendering rules are data, not code. They are a declarative set applied by the engine; teams update them without redeploying the engine.
  4. Renderers declaratively pull their own data. Each Renderer specifies its data dependencies via GraphQL against a central aggregation layer (UBFF); the rendering engine does not hand data to renderers — they fetch for themselves through a typed graph.

What this enables

  • Personalisation changes without frontend deploys. The personalisation service picks a different Entity tree; the same rendering rules pick the right Renderers.
  • Uniform layout control. A/B testing layout variants is a rendering-rule change, not a new fragment.
  • Decoupling teams from placements. A team ships a better CollectionCarouselRenderer; every existing Collection slot gets it on the next deploy without touching the page templates.
  • Cross-cutting concerns stay central. The Rendering Engine owns monitoring, consent, A/B testing, tracking — Renderers need only implement the visualisation.

When to reach for it

  • Pages are heavily personalised and the server can decide structure per request.
  • A reasonably small, stable set of content-entity types exists that can carry most of the page semantic.
  • The org wants per-feature-team ownership but not per-team whole-stack autonomy (see the tradeoff in concepts/micro-frontends).

When not to

  • Most of the page's value is in one-off bespoke layouts (e.g. marketing landing pages, long-form editorial).
  • There's no central team to own the Rendering Engine; it becomes a load-bearing dependency that must be operated.
  • Entity taxonomy is not stable and rework propagates to every Renderer.

Known open questions from the Zalando Part 1 post

  • Rendering rules DSL. Named as "declarative" but its grammar, scope-of-placement, and extensibility are not described. Promised in follow-up posts.
  • Rule-conflict resolution. What wins if two rules match the same (Entity, placement) tuple?
  • Dynamic rule update. Are rules hot-reloaded? Cached? Versioned with Renderer deploys?

Seen in

Last updated · 476 distilled / 1,218 read