FSD Frontend System Design

Part III β€” The Speed Β· Lesson 6.6

Normalization

In one lineStore every entity once, by id, and reference it everywhere else β€” duplicates are bugs waiting to happen.

Where you've seen itSomeone updates their display name, and it changes on their post but not on their three comments below it.

  • Time15 min
  • DiagramNested vs normalized shape, and the update that breaks one of them
  • Chapter6 Β· Database & Caching

Diagram

diagram
NESTED β€” the API shape, copied straight into the store

  posts: [
    { id:'p1', title:'…', author:{ id:'u1', name:'Asha', avatar:'a.png' },
      comments:[ { id:'c1', author:{ id:'u1', name:'Asha' } },
                 { id:'c2', author:{ id:'u1', name:'Asha' } } ] },
    { id:'p2', author:{ id:'u1', name:'Asha' } }
  ]
                     β–²β–²β–²β–²  four copies of the same user

  rename u1 β†’ you must find and update FOUR places
         miss one β†’ inconsistent UI, unreproducible bug
diagram
NORMALIZED β€” one row per entity, ids as the glue

  users:    { u1: { id:'u1', name:'Asha', avatar:'a.png' } }
  comments: { c1: { id:'c1', authorId:'u1', postId:'p1' },
              c2: { id:'c2', authorId:'u1', postId:'p1' } }
  posts:    { p1: { id:'p1', title:'…', authorId:'u1',
                    commentIds:['c1','c2'] },
              p2: { id:'p2', authorId:'u1', commentIds:[] } }

  feed:     ['p1','p2']        ← order lives separately from data

  rename u1 β†’ ONE write. Everything re-renders correctly.
diagram
THE SHAPE β€” entities + order, kept apart

  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ byId:  { id β†’ entity }      β”‚  the data
  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
  β”‚ allIds/feed: [id, id, id]   β”‚  the order, per view
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    lets one entity appear in many lists without copying it

How it works

  1. Flatten by entity type on the way in β€” one dictionary per type, keyed by id.
  2. Replace nested objects with id references, and keep list order as a separate array of ids.
  3. Selectors re-join the pieces at render time (selectPostWithAuthor), memoised so the join isn't repeated.
  4. Normalise once, at the API boundary β€” a small mapper per endpoint, not scattered through components.
  5. Tools do this for you: normalizr, RTK Query's entityAdapter, Apollo's cache, and Relay all implement the same idea.

Trade-offs

Normalize whenSkip it when
The same entity appears in several viewsData is read-only and short-lived
Entities are updated in placeThe response maps 1:1 to one screen
Real-time events patch single recordsYou're rendering a static report
Lists are long and partially sharedThe app is small and flat

Cost, stated honestly: an extra transform layer, selectors to write, and a shape that no longer mirrors the API. For a five-screen app that's overhead; for a feed with live updates it's the difference between correct and haunted.

Interview angle

"A WebSocket sends you an updated post. How does the UI update?"

  • Normalized store: write the entity once by id, and every list referencing it re-renders consistently.
  • Denormalized store: you'd have to locate every copy, and the one you miss becomes an intermittent bug.
  • Keep order arrays separate from entities so live inserts and removals don't disturb the data itself.

Recap

  • One entity, one place, referenced by id.
  • Keep order in separate id arrays so a record can appear in many lists.
  • Normalize at the API boundary; re-join with memoised selectors.