FSD Frontend System Design

Part V β€” The Craft Β· Lesson 10.2

Config Driven UI

In one lineThe server sends JSON describing the screen, and the client renders whatever it's told.

Where you've seen itA food-delivery home screen where the section order changes by city and hour β€” and nobody shipped an app update to do it.

  • Time16 min
  • DiagramConfig β†’ registry β†’ rendered UI
  • Chapter10 Β· Low Level Design

Diagram

diagram
THE PIPELINE

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   JSON    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  Backend   β”‚ ────────► β”‚   Renderer   │──►│  Registry       β”‚
  β”‚  (CMS/     β”‚           β”‚  walks the   β”‚   β”‚ "banner"β†’Banner β”‚
  β”‚   config)  β”‚           β”‚  config tree β”‚   β”‚ "grid"  β†’Grid   β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚ "list"  β†’List   β”‚
                                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                       β”‚
                                                       β–Ό
                                              rendered screen

  New arrangement  = config change  = no deploy
  New COMPONENT    = code change    = deploy (registry entry)
diagram
THE CONFIG

  { "sections": [
      { "type": "banner", "id": "b1",
        "props": { "image": "/diwali.jpg", "cta": "/offers" } },
      { "type": "grid",   "id": "g1",
        "props": { "title": "Whats on your mind?", "source":
                   "/api/categories", "columns": 4 } },
      { "type": "list",   "id": "l1",
        "props": { "title": "Top rated", "source": "/api/top" } }
  ]}
diagram
THE RENDERER β€” small, and the whole idea

  function Renderer({ sections }) {
    return sections.map(({ type, id, props }) => {
      const Component = REGISTRY[type];
      if (!Component) return null;      ← unknown type: skip, never crash
      return <Component key={id} {...props} />;
    });
  }

  Forward compatibility rule: an OLD client receiving a NEW type
  must degrade silently, not throw.

How it works

  1. Define a schema for the config and validate it at the boundary β€” an unvalidated config is a runtime crash waiting for a bad CMS entry.
  2. Keep a registry mapping type strings to components; unknown types render nothing rather than breaking the page.
  3. Version the config so old app versions can be served a shape they understand.
  4. Keep layout in config, behaviour in code. Config that starts carrying conditionals and expressions is becoming a programming language β€” badly.
  5. Cache the config with a short TTL (6.7); it's on the critical path for first paint.

Trade-offs

GainCost
Ship layout changes with no releaseType safety weakens at the boundary
Per-segment, per-region personalisationHarder to debug β€” "why is this here?"
Non-engineers can reorder screensConfig becomes a second codebase
A/B test layouts trivially (4.4)Preview/QA tooling must be built

Where it genuinely pays off: mobile apps behind app-store review, marketing surfaces that change weekly, and multi-tenant products where each customer needs a different arrangement. For a stable internal dashboard it's usually over-engineering.

Interview angle

"How would you build a home screen that changes without a release?"

  • Server returns an ordered list of typed sections; the client maps types to components via a registry.
  • Validate the config, ignore unknown types for forward compatibility, and version the schema.
  • Keep data fetching per section (each declares its own source) so one slow API doesn't block the whole screen β€” stream or lazy-load below the fold.

Recap

  • Config describes arrangement; code provides capability.
  • Unknown types must degrade silently β€” that's what makes old clients safe.
  • Validate and version the config, or you've moved your bugs to a CMS.