Lesson 09 / 24
Flow Diagrams
Building the flow as a graph; searching by computation for unreachable states and dead-end nodes, distinguishing whether a cycle is a flaw, and how path count determines testing load.
Contents
Navigation structure explains how items are reached, but not the decisions inside a task. Borrowing is not a single click: whether the session is active is checked, the record’s status is checked, a reservation is offered if the record is checked out, and the transaction stops if the member has an overdue book. When these branches are described in prose, which state connects to which gets lost, and missing branches go unnoticed.
A flow diagram makes this structure explicit: every state is a node, every transition is an edge. The diagram’s value is not in being a drawing but in being auditable. The graph concept introduced in the Data Structures course is put to direct use here: the moment a flow is written as a graph, the questions asked of it are answered by computation.
The Flow’s Nodes and Transitions
There are three node types, and they must be kept separate.
- State node. A place the user is: the login screen, record detail, borrow confirmation. The user sees something here.
- Decision node. A point where the system or the user produces a branch: is the session active, is there an overdue book. A decision node’s outputs must be mutually exclusive and must cover every case; if only one branch leaves a two-way decision, the diagram is incomplete.
- End node. Where the flow completes. A flow can have more than one ending, and not all of them have to be success; giving up is also an ending, and it is written down.
An ending that is not written is an ending that was not designed. The user still gives up, but where they land is undefined.
Searching for Two Flaws
Once the graph is written, two flaws are searched for by computation. An unreachable state is a node no sequence of transitions from the start can reach; it is a screen that was designed but never wired into the flow. A dead-end node is a node that cannot reach any ending; it is a place the user can fall into and never leave.
// flow.mjs — unreachable states, dead-end nodes, and path counting in the flow graph // Nodes and transitions of the borrowing flow (data built for this lesson) const START = "start"; const ENDINGS = ["borrow successful", "reservation successful", "give up"]; const TRANSITIONS = { "start": ["session active?"], "session active?": ["record status", "login screen"], "login screen": ["record status", "login error"], "login error": ["login screen", "give up"], "record status": ["on shelf", "checked out"], "on shelf": ["overdue book?"], "overdue book?": ["pay fine", "borrow confirmation"], "pay fine": ["borrow confirmation"], "borrow confirmation": ["borrow successful", "system error"], "checked out": ["reservation offer"], "reservation offer": ["reservation confirmation", "give up"], "reservation confirmation": ["reservation successful"], "not a member notice": ["give up"], "system error": [], "borrow successful": [], "reservation successful": [], "give up": [], }; const NODES = Object.keys(TRANSITIONS); // 1. Nodes reachable from the start const traverse = (start, neighbor) => { const g = new Set([start]), stack = [start]; while (stack.length) for (const k of neighbor(stack.pop())) if (!g.has(k)) { g.add(k); stack.push(k); } return g; }; const reachable = traverse(START, (d) => TRANSITIONS[d]); // 2. Nodes that can reach an ending (traversal on the reverse graph) const REVERSE = {}; for (const d of NODES) REVERSE[d] = []; for (const [d, neighbors] of Object.entries(TRANSITIONS)) for (const k of neighbors) REVERSE[k].push(d); const canReachEnd = new Set(); for (const b of ENDINGS) for (const d of traverse(b, (x) => REVERSE[x])) canReachEnd.add(d); console.log("node in out from start to an ending"); for (const d of NODES) { console.log( `${d.padEnd(24)} ${String(REVERSE[d].length).padStart(3)} ${String(TRANSITIONS[d].length).padStart(4)} ` + `${(reachable.has(d) ? "reachable" : "UNREACHABLE").padStart(13)} ${(canReachEnd.has(d) ? "reaches" : "NO EXIT").padStart(9)}` ); } const unreachable = NODES.filter((d) => !reachable.has(d)); const deadEnd = NODES.filter((d) => !canReachEnd.has(d)); console.log(`\nunreachable node : ${unreachable.join(", ") || "none"}`); console.log(`dead-end node : ${deadEnd.join(", ") || "none"}`); // 3. Is there a cycle (a path that loops back) const COLOR = {}; const cycles = []; (function search(d, path) { COLOR[d] = 1; for (const k of TRANSITIONS[d]) { if (COLOR[k] === 1) cycles.push([...path.slice(path.indexOf(k)), k].join(" -> ")); else if (!COLOR[k]) search(k, [...path, k]); } COLOR[d] = 2; })(START, [START]); console.log(`cycle : ${cycles.join(" | ") || "none"}`); // 4. Simple paths from the start to the endings const paths = []; (function walk(d, path) { if (ENDINGS.includes(d)) { paths.push(path); return; } for (const k of TRANSITIONS[d]) if (!path.includes(k)) walk(k, [...path, k]); })(START, [START]); console.log(`\nsimple path count: ${paths.length}`); const lengths = paths.map((y) => y.length - 1); console.log(`shortest path: ${Math.min(...lengths)} transitions, longest path: ${Math.max(...lengths)} transitions`); console.log("\nending path count shortest"); for (const b of ENDINGS) { const these = paths.filter((y) => y.at(-1) === b); console.log( `${b.padEnd(23)} ${String(these.length).padStart(9)} ${these.length ? Math.min(...these.map((y) => y.length - 1)) : "-"}` ); } console.log("\nshortest successful path:"); console.log(" " + paths.filter((y) => y.at(-1) === "borrow successful").sort((a, b) => a.length - b.length)[0].join(" -> "));
node in out from start to an ending start 0 1 reachable reaches session active? 1 2 reachable reaches login screen 2 2 reachable reaches login error 1 2 reachable reaches record status 2 2 reachable reaches on shelf 1 1 reachable reaches overdue book? 1 2 reachable reaches pay fine 1 1 reachable reaches borrow confirmation 2 2 reachable reaches checked out 1 1 reachable reaches reservation offer 1 2 reachable reaches reservation confirmation 1 1 reachable reaches not a member notice 0 1 UNREACHABLE reaches system error 1 0 reachable NO EXIT borrow successful 1 0 reachable reaches reservation successful 1 0 reachable reaches give up 3 0 reachable reaches unreachable node : not a member notice dead-end node : system error cycle : login screen -> login error -> login screen simple path count: 9 shortest path: 4 transitions, longest path: 8 transitions ending path count shortest borrow successful 4 6 reservation successful 2 6 give up 3 4 shortest successful path: start -> session active? -> record status -> on shelf -> overdue book? -> borrow confirmation -> borrow successful
What the Two Flaws Mean
No transition enters the not a member notice node. This screen was designed: it tells a non-member visitor that they cannot borrow, and it connects to giving up. But the flow never lands on this state. Two branches leave the login screen — success and error — and the “this person is not a member yet” case is in neither. The flaw is not in the screen but in the decision node’s missing branch. Unreachable screens are usually evidence that a decision was written incompletely.
No transition leaves the system error node. If the user runs into a system error during borrow confirmation, they fall into this node and cannot reach any ending. The rule set in the Error and Warning States lesson of the Fundamentals of Interface Design course is restated here in the language of graphs: every error state must have at least one exit. The exit can be “retry,” it can be “give up”; having none is not an option.
The start node having an in-count of zero is not a flaw; that is true of a start node by
definition. The start is declared separately so the check can tell the difference.
A Cycle Is Not a Flaw
The check found a cycle: between the login screen and the login error. When the user enters the wrong password, they fall into the error screen, return to the login screen from there, and try again.
This is not a flaw but a designed retry. There is a single criterion for whether a cycle is a flaw: does an edge leave the cycle? The login-error node has two exits — the login screen and give up — so the user is never locked in the loop. A cycle with no exit is the multi-node version of a dead-end node.
The cycle’s second question is how many times it can be looped. Unlimited retries are a problem for password security, zero retries are a problem for usability; in the diagram this is stated as a condition written on the transition, and if the condition is not written, no decision has been made.
Path Count Determines the Testing Load
There are nine simple paths from the start to the endings. This number is directly a testing budget: fully testing the flow means running nine separate scenarios. The shortest path is four transitions (the path to giving up), the longest is eight.
A successful borrow’s shortest path is six transitions, and it has four different paths. The source of the four paths is two binary decisions: is the session active, and is there an overdue book. Two binary decisions produce four combinations; the count stays at four because the system-error branch is not added to it.
The rapid growth of path count gives flow design’s most practical metric: every new binary decision roughly doubles the number of paths to test. Whether a decision is really necessary is asked together with this cost. The “is there an overdue book” decision is necessary because it is the institution’s rule; the “has the user viewed this record before” decision might offer convenience, but it doubles the testing load.
The same computation also suggests a small simplification. The pay-fine node has a single exit, going straight to borrow confirmation; that is, no failure branch is written for after the fine is paid. If payment is an operation that can fail, the diagram is incomplete; if it cannot fail, the node is not a decision but a notice, and it can be dropped from the flow. Single-exit nodes always invite this question.
The Diagram’s Limit
The flow graph shows states and transitions, not time. How long a transition takes, how long the user waits at a node, and what they see while waiting are not in the graph; these belong to the journey map and to component states.
The second limit is that the graph shows the system’s flow, not the flow in the user’s mind. The user does not make a decision called “is there an overdue book”; they are surprised to see their fine on the screen. The diagram being correct does not mean the flow is understandable. Understandability is tested separately, and that is the work of this topic’s last two lessons.
Summary
- A flow diagram is a graph: state nodes, decision nodes, and end nodes; a decision node’s branches must be mutually exclusive and must cover every case.
- An unreachable node is found by forward traversal, a dead-end node by traversing the reverse graph from the endings; in the sample flow, one uncovered a missing branch, the other a dead-end error state.
- A cycle is not a flaw; the flaw is the absence of an edge leaving the cycle and the retry count not being tied to a condition.
- Simple path count gives the testing load; every new binary decision roughly doubles the path count, and a decision’s necessity is asked together with this cost.
- Single-exit nodes point either to a missing failure branch or to a notice that can be dropped from the flow.
Next Step
The flow has been checked and the navigation structure built; both are consistent on paper. A consistent structure is not the same as a usable interface, and the only way to find out is to show it to someone. But what should be shown — a crude box outline, a clickable copy? The next lesson takes on the fidelity-level decision: it computes which question is answered at which level, how many tasks can be tested end to end for how many nodes a prototype covers, and why the two ratios are not equal.
To keep your progress and take notes, Log in
My notes
Log in to take notes.