The Single-Giant-Store Anti-Pattern Even teams that get every decision right so far — the right level, the right tool for the framework, server state kept separate from client state — can still end up with a store that causes constant bugs, because what's inside the store was never designed, only accumulated. One undifferentiated blob, holding nested objects, duplicated data, and computed values stored as if they were the source of truth. This structural mistake causes more real bugs than "should this be in a store at all" ever does. Normalization: stop storing duplicate copies The classic failure: a list of posts, each embedding a full copy of its author. When Alex changes their avatar, every embedded copy needs finding and updating — and it's easy to update some and miss others, leaving the "same" author showing different data in different places. The fix is normalization: store each entity once, by ID, in a flat map, and reference it by ID everywhere it's needed. Now updating Alex's avatar is one write, to one row, and every post referencing author reflects it automatically. Derived state: stop storing what can be computed The second failure is subtler: computing a value once and storing the result as its own piece of state, rather than deriving it live each time it's needed. The moment the source data changes — a new post arrives over a websocket, say — doesn't know to recompute. The new post matches the filter but never appears, because nothing told the derived copy it was now stale. The fix: never store a value that can be computed from other state. Derive it, every time, from the source. In Angular , that's , or an NgRx selector, always reading live from the source signal or store slice — never a second signal manually kept in sync. In React , that's , or a memoized selector (Redux Toolkit's , or a Zustand selector) — computed fresh from the source state on every read, cached only for performance, never treated as its own independent source of truth. Boundaries: one store per real domain The third failure is architectural rather than structural: one universal store instead of one store per genuine feature or domain. If two unrelated features are both writing into the same store slice, that's rarely a sign the store needs to be smarter — it's a sign the boundary between the two features was never actually drawn. The Trade-off: Is Normalizing and Deriving Live Worth the Extra Step? Embedding a full copy of related data, and storing a computed result directly, are both genuinely less code to write in the moment. The cost shows up later, and it shows up as a specific, recognizable kind of bug. The pain, concretely v1: A store holds a list of posts, each embedding its full author object. Fine — one author, a handful of posts, nothing has changed yet. v2: The author's avatar changes. The update only touches the author object embedded in one post's copy — the reducer was never written to walk every post and update every embedded copy. Now the same author shows two different avatars on screen, depending on which post is rendered. v3: Someone adds a field, computed once when a filter changes and stored directly. A new post arrives via a live update to the underlying list, matching the current filter — but was never told to recompute, so the new post silently never appears in the filtered view. v4: An engineer loses an afternoon convinced there are two different "Alex" users in the system, before realizing it's one author, duplicated into an unsynced copy — exactly the class of bug normalization exists to make structurally impossible. Side-by-side Duplicated / stored-derived Normalized / derived live --- --- --- Updating a shared entity (an author's avatar) Must find and update every embedded copy One write, to one row, reflected everywhere by reference A derived view after the source changes Can silently drift out of sync Always recomputed from current source — cannot drift Debugging "why does this look wrong" Requires tracing which copy is stale There's only one copy — nothing to trace Upfront cost None — embed and store directly A normalized shape and a selector, set up once The signal to switch The moment the same entity appears embedded in more than one place in the store — that's the trigger to normalize it into an ID-keyed map. And the moment a piece of state is "compute this from that other state" — that's the trigger to derive it live instead of storing a synced copy. Start simple A store with exactly one entity type, never duplicated, and no derived views, doesn't need normalization or selectors yet. The discipline earns its cost the moment the same entity shows up in two places, or a computed view needs to track a source that can change out from under it. The rule Normalize entities into ID-keyed maps the moment the same entity appears in more than one place — never duplicate a copy that has to be kept in sync by hand. Derive computed views live with selectors or memoization — never store a result t…