Concept CasesILLUSTRATIVETEAM-SPECIFICGENERAL

Case: Implement a Todo List

The simplest concept with identity: a todo has an id, a title and a done flag; the operations are add, toggle, remove, list. The first place a learner meets "one entry per id" — and the place to learn the whole loop on something small enough to hold in one hand.

The moveWorked exampleNext questions

The situation, the reflex, and why it stalls

Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.

The question

A todo list looks too simple to need a method. What does deriving it — instead of typing it — teach that you will need for every concept after it?

The situation

You are learning to program and the exercise says "build a todo app". You have seen a hundred of them. You are not sure what there is to think about, and you are slightly embarrassed to be stuck on which part to type first.

The reflex

Copy the canonical todo app — an array of strings, a push on add, a splice on remove — and get to the interesting part, which you assume is the framework.

Why it stalls

An array of strings has no identity. Two todos titled "call mum" are indistinguishable; removing one removes whichever the index points at, and after a re-render the index has moved. The bug looks like the framework's fault and is the data's.

What the reflex produces — and fails to produce
  • An array of strings has no identity. Two todos titled "call mum" are indistinguishable; removing one removes whichever the index points at, and after a re-render the index has moved. The bug looks like the framework's fault and is the data's.
  • There is no "done" state because strings cannot carry one, so the first real feature — marking a todo complete — forces a rewrite of the shape you copied, and the copy taught nothing about how to choose a shape.
  • The exercise felt too simple to need rules, so none were written, and "can two todos have the same id?" is answered by accident on the day two clicks race.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Run the whole loop at full size on the smallest possible concept, so that every step is visible and none is skipped for being obvious. Meaning: a list of things the user intends to do, each of which can be marked done. That sentence already says there is a per-item flag and a per-item identity.
  • Discover the state and challenge it: id, title, done — and ask what an id is for before you accept it. The answer — so that toggle and remove can name a todo without depending on its position or its title — is the first time a learner meets identity as a design decision rather than a database column (Does It Have Identity?).
  • Write the operations, the rules and the examples, and let the rule "one entry per id" appear from the example "add the same id twice", exactly as "one entry per product" appeared in the cart.
  • Choose the representation from the operations — array while the list is short and order matters, map when lookup by id dominates — implement toggle, trace it, and stop. The engineering questions (persist across reload, share across devices) are the ladder's next levels and each has a trigger.

Meaning and state — the id is a decision, not a column

The canonical copy uses an array of strings, and the reason it fails is that strings have no identity. The meaning — each of which can be marked done — needs to say which one, and "which one" is what an id is. The decision device asks the question the copy skipped.

The rest of the state canvas is short. Three fields survive; three are derived, deferred or dropped; and the exercise of challenging six fields on a todo is the rehearsal for challenging nine on a cart.

  • id — keep: toggle and remove need to name a todo independent of position and title.
  • title — keep; done — keep: the meaning names both.
  • createdAt — depends: only if the list is sorted by it; position — derive from array order until "reorder" exists; completedAt — drop: no V1 operation reads it (Challenging Unnecessary State).
How does toggle name a todo?

toggle needs to say which todo it means. What identifies one?

The title

when Titles are unique and never edited.

cost Two "call mum" entries are one todo; renaming breaks every reference.

The array index

when Nothing is ever removed or reordered.

cost Remove the first todo and every later index is wrong — the remove-then-toggle example fails.

A generated id

when Todos can be removed, reordered or duplicated by title — which is every real list.

cost One more field, and a generator that must not repeat — a counter in memory, something sturdier once the list survives a reload.

Operations, the rule from the example, and the state change

Four operations with their errors decided. The rule that matters — one entry per id — was not in the meaning; it appeared when the example "add with id 1 again" was written and the after had to be chosen. That is the same discovery the cart makes with "add Laptop again", one concept earlier and one size smaller.

The state change shows toggle on the smallest list that has something to toggle. Nothing but one flag moves, and the changed list says exactly that.

  • add(title): rejects an empty or whitespace-only title; assigns the next id; done starts false.
  • toggle(id): flips done on exactly one todo; unknown id → rejected, because flipping nothing is a silent lie.
  • remove(id): unknown id → no-op, because the caller wanted it gone and it is gone — the cart's choice, made for the same reason.
  • list(): returns the todos in insertion order; an empty list is an empty list, not null.
toggle 1
before
[ { id: 1, title: "call mum", done: false } ]
toggle(1) →
after
[ { id: 1, title: "call mum", done: true } ]
what changed todos[0].done: false → true · todos.length, id, title: unchanged
One entry per id

rule No two todos share an id; adding with an existing id is rejected rather than duplicated or overwritten.

becomes validation Before appending, look for an existing todo with the same id; if found, reject.

becomes code
if exists(todos, t => t.id == id): reject "duplicate id"
append(todos, { id, title, done: false })

Representation, toggle in pseudocode, and the trace that kills the index

An array of { id, title, done }, because the list is short and rendered in order; toggle scans by id in O(n), which at any todo list's size is nothing. The alternative map by id is annotated, not chosen (Why This Data Structure?).

The trace runs toggle after a removal — the example that separates an id from an index. Follow the lookup: it finds todo 2 by comparing ids, not by counting.

remove 1, then toggle 2
  1. inputtodos = [ {1, call mum, false}, {2, buy milk, false} ]; remove(1); toggle(2)
  2. lookupafter remove: todos = [ {2, buy milk, false} ]; toggle scans for id == 2 → found at index 0
  3. branchtodo found → flip; an index-based toggle(2) would have looked at index 2, found nothing, and failed here
  4. mutationtodos[0].done: false → true
  5. output{2, buy milk, true} — the todo the user meant
How far the todo list goes
  1. V0 — in memory
    The four functions over an array; examples as tests.The behaviour and the id rule are testable with no page around them.
  2. V1 — in the page
    Component state holds the array and calls the functions.A person needs to click; the functions do not change (The Framework Comes Last).
  3. V2 — browser storage
    Serialise on change, load on start; the id generator must survive the reload.A reload emptying the list is the first thing a user notices; the counter resetting to 1 is the first bug (Persistent Client State in Frontend).
  4. V3 — server rows
    A todo table with a primary key; the id rule becomes a constraint; an owner column appears.The list must be seen from a second device — the reading that introduces an owner and an API.
toggle
1function toggle(todos, id):
2 todo = find entry in todos with todo.id == id
3 if todo is missing: reject "unknown id"
4 todo.done = not todo.done
5 return todo

Find, branch, flip, return. The find is a loop comparing ids — the same primitive as the cart's addItem, one concept earlier (Go One Primitive Lower).

The implementation ladder

Concept, examples, pseudocode, code, tests, production — for the concept this lesson is about. Code is the fourth tab, not the first.

concept Todo List beginner
Build it step by step →

Todo List = A collection of tasks, each of which is either open or done, that the user adds to, ticks off, renames, removes and looks through.

Identity, ownership, lifetime
  • Does a todo have identity? Yes. Two todos titled "Buy milk" are two todos; ticking one must not tick the other. Titles are not identity — they repeat and they change — so each todo needs an id the moment it can be referred to again, which is the moment it exists.
  • Does the list have identity? Weakly. V0 has one list and the variable is its identity. A second list ("Work", "Home") or a second user gives the list an id and an owner.
  • Who owns it? One person in V0 through V3. The owner becomes state when a second person can see the list, and becomes a rule ("only members may edit") when the list is shared (V4).
  • How long does it exist? A todo lives from add until remove; ticking it off does not end it, it changes it. The list lives as long as the person wants it — which is longer than the page, so persistence arrives early.
  • Should it survive reload? A todo list that forgets on refresh is not a todo list. V1 in memory says no and is only a step; V2 browser storage is the first version a person could use.
  • Should it survive login? Only once there is a login. Then the list follows the user across devices (V3) and the browser copy becomes a cache.
State it must remember
  • todoscollection of TodokeepThe list is its todos.
  • todos[].idid, unique within the listkeepToggle, rename and remove need to say which todo; the title cannot, because titles repeat.
  • todos[].titlenon-empty stringkeepWhat the task is.
  • todos[].donebooleankeepOpen or done is the whole point of a todo.
  • nextIdintegerkeepSomething has to hand out ids that never repeat.
  • openCountintegerdropEvery screen shows "3 items left".
  • todos[].createdAttimestampdependsSorting by newest, or "added yesterday".
  • todos[].completedAttimestamp or emptydependsIt would replace done — done is "completedAt is set".
Operations
  • create Add the new todo, with its id
  • update Toggle done the updated todo
  • delete Remove the updated list
  • update Rename the updated todo
  • read List the matching todos in the order they were added
Rules that must always hold
  • Every id is unique within the list, forever.
  • A title is never empty.
  • Operating on a missing id is an error.
  • Done is exactly one of two states.

How to do it

Most important first.

  • Write the meaning and circle the words that imply state: "each" (identity), "marked done" (a flag). If a word in the sentence has no field, either the sentence or the state list is incomplete (What Must It Remember?).
  • Challenge: id (keep — toggle and remove need it), title (keep), done (keep), createdAt (depends — only if the list is sorted by it), position (derive — array order until a "reorder" operation exists), completedAt (drop — no V1 operation reads it).
  • Write the operations with their errors: add(title) rejects an empty title; toggle(id) and remove(id) reject an unknown id — or no-op; decide, write the example, and notice the cart made the same choice for remove.
  • Write the examples as before → operation → after, then write the add-twice one and discover the id rule.
  • Implement toggle in pseudocode, then in one language, and run the examples. Then ask what breaks if ids are the array index — and answer with the example "remove the first, then toggle the second".
  • Write the ladder: in memory → browser storage → server rows, each with the reading that triggers it, and build only the first.

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • Meaning: a list of things the user intends to do, each of which can be marked done. Identity: each todo has one, because "mark this one done" must survive the list being re-sorted or an identical title being added. Owner: one user in V1. Lifetime: from add until remove; done todos are still todos.
  • State: id (keep), title (keep), done (keep), createdAt (depends), position (derive from array order), completedAt (drop). Three kept fields, which is the whole shape: { id, title, done }.
  • Operations: add(title) → the new todo; toggle(id) → the todo with done flipped; remove(id) → the list without it; list() → the todos in order. Rules: title is non-empty; ids are unique — one entry per id; toggle flips exactly one todo; remove of an unknown id is a no-op (chosen, as for the cart).
  • Examples: [] → add "call mum" → [{1, call mum, false}]; → toggle 1 → [{1, call mum, true}]; → toggle 1 → [{1, call mum, false}]; [{1, …}] → add with id 1 again → rejected — the example that produced the id rule; [{1, …}, {2, …}] → remove 1 → [{2, …}], and toggle 2 still finds todo 2 — which it would not if 2 were an index.
  • Representation: an array of { id, title, done }, because the list is short, is rendered in order, and toggle's O(n) scan by id is invisible at that size. A map by id would make toggle O(1) average and the uniqueness structural — the right answer once the list is long or lookup dominates. The generated id: a counter in V1, replaced by something not reset by a reload at V2 (Array Cart vs Map Cart).

How you know it worked

What now exists that did not before, and what question you can now ask.

  • Every field has an operation that reads it, and the id has a sentence saying what it is for.
  • You can write the example that breaks "id is the index" and your implementation passes it.
  • The rule "one entry per id" exists in words, as a check and as a test, and you found it from an example rather than from a lecture.
  • When someone asks "why not just an array of strings?", you answer with the remove-then-toggle example rather than with a preference.

The questions you can now ask

The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.

Next questions
  • ?What does the id let me do that the title and the position could not?
  • ?Which example reveals the rule "one entry per id" — and which line of code encodes it?
  • ?What breaks if the id is the array index — and what is the smallest example that shows it?
  • ?Is the list short enough and ordered enough that an array is right — and at what point would I say otherwise?
  • ?What reading would justify saving the list beyond the page's lifetime?

What can go wrong

How the move itself fails
  • The loop is run at full ceremony forever: a todo with a state canvas, six operations, a persistence ladder and no code after a day. The concept is small so that the loop takes an hour; when it takes longer, the loop is being performed rather than used.
  • Identity is skipped because "it's just a demo", and the demo grows into the app; the first re-sort breaks every toggle, and the fix is the rewrite the derivation would have avoided.
  • The map is chosen for a list of five todos "for efficiency", and now insertion order — the thing a todo list is — depends on the language's map guarantees.
What the move costs
  • Deriving a todo list takes longer than copying one, for a concept every tutorial has already built; the payoff is entirely in the concepts after it.
  • A generated id means one more thing to get right — uniqueness across reloads — that an array of strings did not have.
  • Choosing "remove of unknown id is a no-op" makes a caller's typo invisible; choosing "error" makes a double-click into a failure. Either is defensible and one must be chosen.
Misreads
  • "A todo list is too simple to teach anything." It is the smallest concept that has identity, a per-item flag and a uniqueness rule — the three things the cart, inventory and the job queue all have, at a size where they can be held in one hand.
  • "Use the index as the id, it is simpler." It is simpler until the first removal, after which every later id is wrong; the remove-then-toggle example is the falsification.
  • "The framework is the interesting part." The framework holds the list in component state and calls add, toggle and remove; the four functions survive every framework change, which is why they come first (The Framework Comes Last).

Where this applies

Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • ILLUSTRATIVEThe todos "call mum", ids 1 and 2, and a list of five are invented; the size at which a map beats an array for a todo list is one nobody reaches.
  • TEAM-SPECIFICA beginner should run every step out loud; an experienced engineer reads this lesson for the shape of the argument and applies the loop at full size to the first concept they have not built before.
  • GENERALIdentity, a per-item flag, and one-entry-per-key recur in almost every concept with a collection; the todo list is where they cost least to learn.

Where the depth lives

This domain asks the question and hands the answer off by name.