FullStack Course LogoFullStack Course
Module: Machine Coding
Machine Coding·238·13 MIN READ

238: Data Modeling, Normalization, Derived State, and Reducers

TOPICS COVERED: Data Modeling, Normalization, Derived State, and Reducers

Learning outcomes

By the end of this lesson, you can:

  • explain and apply source of truth in a realistic implementation;
  • explain and apply entity normalization in a realistic implementation;
  • explain and apply finite states in a realistic implementation;
  • explain and apply reducers in a realistic implementation;
  • explain and apply undo and history in a realistic implementation.

Prerequisites and retrieval

This lesson assumes the earlier 01–06 foundation and the preceding lessons in this module. Before you read, retrieve one concrete example from a previous project in which one of these concerns appeared. Perhaps two screens had to show the same record, a loading flag could contradict an error, or a derived count became stale after an update. The point is not to memorize terminology. It is to make a defensible state-design decision in a timed machine-coding exercise while keeping the result readable, accessible, testable, and easy to extend under interview pressure.

Terminology

  • Source of truth: Store the smallest set of mutable facts that users or external data can change. Values that can be calculated from those facts are derived state, not additional sources of truth.
  • Entity normalization: For relational client data, store entities by ID together with ordered ID lists. This is useful when several screens update or reference the same records. It is not a requirement for every small array.
  • Finite states: Model mutually exclusive states explicitly instead of allowing scattered booleans to describe impossible combinations.
  • Reducers: A reducer is useful when several events update related fields under shared invariants. It receives state and an action, then returns the next state without performing side effects.
  • Undo and history: A history feature needs an explicit snapshot or command strategy, along with a bound on memory and a clear definition of which changes are undoable.
  • Persistence: LocalStorage persistence is a side effect. Stored data may be stale, malformed, or written by an older version of the application, so it must be treated as untrusted input.

Mental model

Treat Data Modeling, Normalization, Derived State, and Reducers as a design problem with observable inputs, outputs, invariants, and failure modes. The model you choose determines whether a machine-coding solution remains predictable when the happy path ends. Store each source fact once, then derive the views, totals, labels, and enabled states that depend on it. Duplicating those values creates synchronization work and gives bugs more than one place to hide.

A strong implementation makes assumptions visible and narrows uncertainty at system boundaries. Types, constraints, tests, metrics, and diagrams are all useful evidence: they help another developer verify why the design is safe rather than asking that developer to infer it from a collection of event handlers.

A useful interview and production sequence is:

text
requirement -> constraints -> model -> implementation -> failure analysis -> verification

Do not jump from a requirement directly to a library call. First write down what must remain true. Then choose the mechanism that enforces those rules. For example, “the card appears in exactly one column” is an invariant; normalization and a reducer are implementation choices that can help enforce it.

Deep dive

1. Source of truth

The recurring problem is duplicated mutable state. If you store both a line-item list and a total, or both a selected entity and a copy of that entity, every update must remember to keep the copies synchronized. One missed update produces a UI that is internally inconsistent even though each individual value looks plausible.

Store the minimal mutable facts that users or external data can change. Compute totals, filtered lists, labels, and enabled states during render or through memoized derivation when the calculation genuinely needs it. The question to ask is: if this value changes, who owns the change? If the answer is “it is calculated from other state,” it generally should not be another writable state field.

There are legitimate exceptions. A server-provided total may be a source fact rather than a client calculation, and a deliberately cached result may be stored if its invalidation rule is explicit. Do not treat “derived” as a commandment; treat it as a way to make ownership and invalidation clear.

Decision rule: Use source of truth deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

2. Entity normalization

An array is often the clearest model for a small, local list. Normalization becomes useful when the same record appears in multiple places or when updates should address one record by ID without walking and replacing copies throughout the state tree.

For relational client data, store entities by ID plus ordered ID lists when many screens update the same records. For example, an ordered list can answer “which cards are in this column?” while the entity map answers “what are the current fields for card c-17?” Updating the card then changes one entity, and the existing order remains independent from the entity’s contents.

The trade-off is real: normalization adds lookup code and requires invariants such as “every ID in an order list resolves to an entity” and “an entity is not listed twice unless that is intentional.” Small local lists can remain arrays; normalize only when ownership and update patterns justify the added model.

Decision rule: Use entity normalization deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

3. Finite states

Scattered booleans invite states that should not exist. isLoading, hasData, and hasError can all be true unless every transition handler carefully coordinates them. A reader then has to reconstruct the state machine from effects and conditionals.

Model mutually exclusive states explicitly instead. A reducer or discriminated union can represent idle, loading, success, and error, or the transitions between draft and saved, without permitting contradictory combinations. Each state can carry only the data that makes sense in that state, such as a result for success or an error for error.

Finite states do not remove every product decision. You still need to decide whether stale data remains visible while a refresh is loading, or whether a retry returns to loading. Write those decisions down so the UI and reducer agree about the allowed transitions.

Decision rule: Use finite states deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

4. Reducers

Reducers earn their complexity when several events update related fields under shared invariants. A move, create, or delete operation may need to update an entity map, one or more ordered lists, a selection, and history together. Independent setters make it easier for those updates to drift apart.

Actions should describe what happened, such as cardMoved or cardDeleted, rather than prescribing a sequence of field assignments. The reducer should remain pure: given the same state and action, it returns the same next state and does not write to LocalStorage, make a request, read the clock, or mutate the existing state. Perform those side effects at the boundary that owns them, then dispatch the resulting domain event.

Decision rule: Use reducers deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

5. Undo and history

Undo is not merely a second button next to an update. You need to define which user action creates a history entry, whether consecutive keystrokes are grouped, what happens after undo followed by a new edit, and how much memory the feature may consume.

History features need an explicit snapshot or command strategy and bounded memory. Snapshots are straightforward and easy to restore, but copying an entire large state tree after every keystroke can be expensive. Commands can use less storage, but they require reliable inverse operations and careful handling of schema changes. Do not add undo by copying an entire large state tree after every keystroke without considering cost.

Decision rule: Use undo and history deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

6. Persistence

Persistence changes the trust boundary. LocalStorage may contain data from an old deployment, a partially written value, or arbitrary text. A successful JSON.parse does not prove that the result has the shape your reducer expects.

LocalStorage persistence is a side effect and can contain stale or invalid data. Parse stored values, validate the resulting shape, version the data when the shape may evolve, and fall back to a safe initial state when recovery fails. Keep storage synchronization outside pure state logic. Also remember that local persistence is local to a browser profile; it is not a substitute for server persistence, authorization, or conflict resolution.

Decision rule: Use persistence deliberately when it makes the contract or invariant easier to prove. If it only reduces typing while hiding an assumption, prefer the more explicit design.

Worked example

Consider a timed machine-coding exercise that must remain readable, accessible, testable, and easy to extend under interview pressure. Start by expressing the requirement in one sentence. Then list the input and output contracts, and identify which concept above owns each failure mode.

The important design move is separation. Parsing and validation belong at the boundary. Domain rules belong in the domain or service layer. Persistence rules belong in the database or repository. Presentation rules belong in the client. Mixing these concerns can make a happy-path demo look shorter, but it makes edge cases much harder to reason about and test.

ts
export async function handleRequest(input: unknown) {
  const command = parseCommand(input);
  const result = await service.execute(command);
  return toHttpResponse(result);
}

Here, unknown makes the boundary explicit: the handler cannot assume that the incoming value is already a valid command. parseCommand owns that conversion or rejection, service.execute owns the domain operation, and toHttpResponse translates the result for the transport. The example is intentionally small; its value is the ownership boundary, not the amount of framework code around it.

Walk the example with at least four cases: the normal path, an empty or missing value, a duplicate, retry, or concurrent path where relevant, and a dependency failure. For each case, state which layer detects the problem and what the caller observes. A malformed input should not be allowed to masquerade as a database failure, and a dependency failure should not be silently reported as a successful response. This is the level of explanation expected in a senior code review or technical interview.

Production perspective

Production correctness is broader than “the code works on my machine.” Ask how the design behaves during deploys, retries, partial failure, stale clients, concurrent requests, malformed data, schema changes, and high cardinality. State models that are clear at ten records may need different lookup or persistence strategies at ten million. Prefer explicit contracts, bounded resource usage, structured errors, and measurable behavior. Optimize only after you can identify the bottleneck or risk with evidence.

When the topic involves an external dependency, define a timeout and cancellation strategy. When it involves persistence, define transaction and consistency expectations. When it involves user-visible state, define loading, empty, error, stale, and success states rather than letting incidental booleans define them. When it involves security, assume the client can be modified and all network input is untrusted. Client-side state can improve interaction, but it cannot enforce server-side authorization.

Guided lab

Model a Kanban board with normalized cards, column ordering, derived counts, and a reducer for move, create, and delete operations. Persist a versioned subset to localStorage and recover gracefully from malformed stored JSON.

The model should make the relationships inspectable: columns own ordered card IDs, the normalized card collection owns card fields, and counts are derived from the current ordering rather than stored as another mutable fact. Decide what should happen when a move references a missing card or column, and make that behavior part of the contract instead of leaving it to an accidental exception.

Complete the lab with this discipline:

  1. Write the requirement and two non-requirements.
  2. List input, output, and error contracts before implementation.
  3. Implement the smallest correct vertical slice.
  4. Add at least one invalid-input test and one edge-case test.
  5. Instrument or inspect the behavior instead of guessing.
  6. Refactor one hidden assumption into an explicit type, constraint, function, or configuration.
  7. Explain one alternative design and why you did not choose it.
  8. Record a short “what would break at 10× scale?” note.

For the persistence portion, test both valid versioned data and malformed JSON. A parse failure should lead to a safe recovery path, not a blank screen or a reducer receiving an unexpected object. Inspect the resulting state after each action so you can verify that a card is not left in two columns and that deleting a card removes every relevant reference.

Edge cases and failure modes

  • Source of truth: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. In particular, verify that a derived total changes when its source facts change and cannot become a second stale value.
  • Entity normalization: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Check dangling IDs, repeated IDs, and updates to an entity referenced by more than one view.
  • Finite states: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Check that impossible combinations such as success data with an unhandled error cannot leak into the UI.
  • Reducers: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Check unknown or invalid actions, immutability, and invariants spanning more than one field.
  • Undo and history: test absence, malformed input, duplicates, ordering/concurrency where applicable, and behavior at the smallest and largest credible sizes. Check empty history, repeated undo, redo invalidation after a new edit, grouping behavior, and the history memory bound.

Common mistakes and debugging

  • Solving the example instead of the requirement: a copied pattern can be syntactically correct but architecturally wrong.
  • Hiding uncertainty with assertions, broad exception handlers, permissive schemas, or “temporary” any values.
  • Testing only the happy path and therefore discovering contracts only after integration.
  • Optimizing before measuring, or selecting a scalable mechanism without a scale requirement.
  • Letting client-side behavior stand in for server-side authorization, validation, or persistence guarantees.

For debugging, reproduce the smallest failing case first. Inspect the actual value or execution plan, trace the boundary where the invariant first becomes false, and fix the owning layer rather than adding a downstream patch. If the UI shows the wrong count, inspect the source collection before changing the count calculation. If a normalized view is missing a card, inspect both the ID list and the entity map. If a reload breaks the board, inspect the stored version, parsed value, validation result, and migration or fallback path in that order.

Interview questions

  1. What problem does Source of truth solve, and what trade-off or failure mode would make you choose a different approach?
  2. What problem does Entity normalization solve, and what trade-off or failure mode would make you choose a different approach?
  3. What problem does Finite states solve, and what trade-off or failure mode would make you choose a different approach?
  4. What problem does Reducers solve, and what trade-off or failure mode would make you choose a different approach?
  5. What problem does Undo and history solve, and what trade-off or failure mode would make you choose a different approach?

Checkpoint

Without notes, explain Data Modeling, Normalization, Derived State, and Reducers to another developer in five minutes. Your explanation must include one invariant, one edge case, one production failure mode, and one alternative design. Then implement a small example without copying the lesson code. Be prepared to explain where validation happens, which values are stored versus derived, and how you would observe a broken invariant during debugging.

Mastery checklist

  • I can define the core terms precisely.
  • I can choose a design from requirements instead of from habit.
  • I can implement and test the normal path and edge cases.
  • I can explain the runtime, storage, or complexity cost.
  • I can identify which layer owns validation, errors, and recovery.
  • I can compare at least two reasonable alternatives.
  • I can explain how the design changes at larger scale or stricter reliability.

References

Reader page: /machine-coding/lesson/238/data-modeling-normalization-derived-state-and-reducers