Skip to content

Fix the class, not the instance

Ask what let the bug be written, and guard the whole class at the highest rung that can see it.

Predict first

Before you read: predict

ExamHall. Last month the proctor console stored 15 minutes of extra time for a candidate with no approver (EXH-2491). MR !2792 is merged: grantExtraTime now takes a type that requires an approver.

In the project's tsconfig, allowJs is on and checkJs is off. CI runs tsc --noEmit on main. Next week the disability office's CSV for the June sitting arrives with 6 rows, and 2 of them have an empty approver column.

What does tsc report, and how many grants with no approver does the import store?

Code for this question
// src/accommodations/grant.ts (MR !2792)type Grant = { candidateId: number; examId: number; minutes: number; approver: string };export async function grantExtraTime(db: Db, g: Grant): Promise<void> {  await db.query(INSERT, [g.candidateId, g.examId, g.minutes, g.approver]);} // src/accommodations/import.js (JavaScript, not changed by the MR)/** @param {any} db @param {{ candidate: number, exam: number, minutes: number, approver: string }[]} rows */export async function importCsv(db, rows) {  for (const r of rows) await grantExtraTime(db, { candidateId: r.candidate, examId: r.exam, minutes: r.minutes, approver: r.approver || null });}
Pick one

The 30-second version

After a fix, ask what let anyone write the bug, then move the guard up the ladder. A comment guards nothing; a regression test, only the path it runs; a type, code the compiler checks; a lint rule or constraint, every file or write it sees; a design, the step itself. Search for the same shape before closing.

Ask what let the bug be written, and guard the whole class at the highest rung that can see it.

How it works

Step 1 of 11

Ask what made it possible. Once the fix is proven, ask what let any code write this bug, not only which line did.

Analogy: A petrol station after a driver fills a diesel car with unleaded (fuel retail)

On the forecourt In the code
a "Diesel only" sticker inside the filler flap, which no pump reads a comment (step 3)
re-checking pump 4, where last week's mistake happened a regression test of one path (step 4)
a pay-at-pump reader that unlocks only the fuel registered to the car; a driver paying cash at the kiosk never meets it a type: callers the checker sees (step 5)
an inhibitor in the car's filler neck that opens only for a diesel nozzle, at any pump a constraint at the store (step 7)
a diesel nozzle too wide to enter an unleaded car's filler neck a design where the mistake cannot be made (step 8)
after the misfuel, checking every pump and every way of paying searching by the class's shape (step 10)

In real terms: after fixing a bug, rank the guards by how many ways of writing it each one blocks, now and later, and add the highest that can see it.

Breaks down when: the wide nozzle blocks only one mistake; an unleaded nozzle still fits a diesel car. In code too, a rung that blocks one bug may not see the next (step 9). Wrong inference: "the design rung always wins."

Contrasting case: a fee the business set too low is a decision, not a class of mistake; changing the value is the whole fix.

Examples, run for real

4.1 ExamHall EXH-2490: "Released: no score"

Exam 12's results page showed two candidates "Released: no score". Three writers release results: the proctor console (one attempt), the scheduled release job (the whole exam) and a support engineer's psql backfill. Each rung was added alone, then each writer ran once on a fresh copy of exam 12, whose attempts 2 and 4 are not scored yet.

4.1 rung        console           job               psql              released with no score
4.1 comment     1                 2                 2                 5
4.1 test        0                 2                 2                 4
4.1 type        0                 0                 2                 2
4.1 constraint  0 (refused 23514) 0 (refused 23514) 0 (refused 23514) 0
4.1 test: the MR's regression test passes: true · type: tsc before the fix TS2322 at src/console/release-one.ts:3, TS2322 at src/jobs/release-due.ts:3; after: no diagnostics

The comment changes nothing (step 3). The regression test fixes the console and passes, while the job and psql repeat the bug (step 4). The type refuses both TypeScript writers until they skip unscored rows; psql is not TypeScript (step 5). The constraint refuses all three (step 7).

4.2 MR !2790: the guard the class needed

4.2 MR !2790 adds released_has_score: added, the stored rows pass
4.2 the job as it was: refused 23514 · released 0 of 5
4.2 the job with AND score IS NOT NULL: released 3 · not scored, waiting 2

The constraint refused the job's whole statement, scored results included, so the failure is loud; the job then skips unscored rows (step 7). The console's regression test stays (step 11).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 5

A payments ledger books money through ledger.credit(accountId: string, amountPence: number), which inserts one row into ledger_entries (account_id text, amount_pence integer). Last week the chargeback handler passed a figure in pounds, so a £40 chargeback was booked as 40 pence; that handler now converts. The refunds and card-sales modules call credit too. Next quarter another team's payouts module will call ledger.credit(acct, payout.amount) for a £250 payout, where payout.amount holds pounds.

With only one of these guards added today, which one stops that call before an entry is stored?

Pick one

Question 2 of 5

A sold ticket must have a seat. Last week the box office's sell() marked a ticket sold before a seat was chosen; the fix makes sell() refuse a ticket with no seat. Before closing the ticket, Ivo looks for the same bug elsewhere. The service's statements that touch tickets, by file:

src/boxoffice/sell.js    UPDATE tickets SET status = 'sold', seat_id = $2 WHERE id = $1
src/checkout/confirm.js  updateTable(db, 'tickets').set({ status: 'sold' }).where('order_id', '=', orderId)
src/venue/remap.js       UPDATE tickets SET seat_id = NULL WHERE seat_id = ANY($1)
src/partners/import.js   INSERT INTO tickets (order_id, status, seat_id) SELECT order_id, 'sold', seat_ref FROM partner_sales
src/refunds/refund.js    UPDATE tickets SET status = 'refunded', seat_id = NULL WHERE id = $1
src/reports/daily.js     SELECT count(*) FROM tickets WHERE status = 'sold'
src/admin/purge.js       DELETE FROM tickets WHERE created_at < $1

Tickets are created with no seat, and a seat is added when the buyer picks one; a partner's row has no seat_ref until the partner assigns seats.

Which search lists every statement above that can leave a sold ticket without a seat, and the fewest that cannot?

Pick one

Question 3 of 5

A video-streaming service's postmortem: 30 episodes went live with no age rating after the bulk-upload tool skipped the rating field, and children's profiles listed them for two days. Episodes also reach the catalogue through the single-episode form and the partner feed.

Which action item from the draft does the review keep as the postmortem's main follow-up?

Pick one

Question 4 of 5

A shop keeps discontinued products in its products table, with discontinued_at set. Last week search showed discontinued products, and the fix added AND discontinued_at IS NULL to the search query. The storefront listing runs SELECT id, name FROM products WHERE shop_id = $1. Next month the data team adds a sitemap export: a file, sitemap.sql, that a scheduler runs, selecting from products.

Which single change leaves no path, the sitemap included, returning a discontinued product?

Pick one

Question 5 of 5

A music service's chart page put a track with 9 plays above one with 10. src/charts/top.js sorted the play counts with .sort(), which compares numbers as text; the fix passes (a, b) => b - a.

Which one guard, added today, reports the other places in the code base that already sort numbers this way, and any new one in a later merge request?

Pick one

Go deeper

Pro tip
  1. Before merging a class guard, run it against the instance's original bug: put the old code back on a branch and check the guard refuses it. A guard never seen to refuse anything may guard nothing. When: adding a constraint, type or lint rule after an incident (steps 7, 11). Verify: the release job's original statement against released_has_score → refused 23514 · released 0 of 5. Status: RAN.
Source

Tool behaviour is version-specific; the pack ran PostgreSQL 18.3 (PGlite 0.5.8), TypeScript 6.0.3, ESLint 10.11.0 and git 2.52.0. Valid as of 2026-10-02.

  • Google SRE book, ch. 15, Postmortem Culture: Learning from Failure (John Lunney and Sue Lueder): https://sre.google/sre-book/postmortem-culture/ (sre-postmortem-culture.txt), read 2026-10-02. The curriculum row's one source. Quoted: "the postmortem is seen by engineers as an opportunity not only to fix a weakness, but to make Google more resilient as a whole."; "A postmortem is a written record of an incident, its impact, the actions taken to mitigate or resolve it, the root cause(s), and the follow-up actions to prevent the incident from recurring."; "You can’t "fix" people, but you can fix systems and processes to better support people making the right choices when designing and maintaining complex systems."
  • PostgreSQL documentation, 5.5. Constraints: https://www.postgresql.org/docs/current/ddl-constraints.html (pg-ddl-constraints.txt), read 2026-10-02. Quoted: "If a user attempts to store data in a column that would violate a constraint, an error is raised."
  • PostgreSQL documentation, CREATE VIEW: https://www.postgresql.org/docs/current/sql-createview.html (pg-create-view.txt), read 2026-10-02. Quoted: "The view is not physically materialized. Instead, the query is run every time the view is referenced in a query."
  • TypeScript, TSConfig option checkJs: https://www.typescriptlang.org/tsconfig/checkJs.html (ts-tsconfig-checkjs.txt), read 2026-10-02. Quoted: "When checkJs is enabled then errors are reported in JavaScript files." Supports step 5 and the opening question t1, which is keyed by its run; keys nothing.
  • ESLint, no-restricted-syntax and Configure Rules: https://eslint.org/docs/latest/rules/no-restricted-syntax and https://eslint.org/docs/latest/use/configure/rules, read 2026-10-02 from their source in the eslint/eslint repository at v10.11.0 (eslint.org refused connections from this machine; eslint-no-restricted-syntax.txt, eslint-configure-rules.txt, each with its Via: line). Quoted: "this rule allows you to configure the syntax elements you want to restrict use of"; "To disable rule warnings in a part of a file, use block comments". Support step 6; key nothing.
  • git, git-grep: https://git-scm.com/docs/git-grep (git-grep.txt), read 2026-10-02. Quoted, for --no-index: "Search files in the current directory that is not managed by Git". Background for how P2's searches run; keys nothing.

Checked against the tools and docs as of 2026-10-02.