Skip to main content

On a phone? for plain text and static diagrams.

The Development Checklist

Development does not start when you open your IDE, and it does not end when the code compiles.

What happens next, and what do I have to show for it? Press Play, or click any stage. Each stage hands the next one a piece of evidence. Confidence is the sum.
Stage 1 of 9

Evidence you hold (1 of 9)

  • The ticket

Still a guess if you stop here

  • Rules and acceptance criteria
  • The affected components
  • The smallest working change
  • Green tests for the rule
  • Old journeys still pass
  • A clean diff
  • CI, review, approval
  • Evidence others can trust

A ticket, a dropdown, and the word “Done”

A developer picks up a ticket on a Tuesday morning:

Add Approved and Rejected status to the application.

They open VS Code, add two options to the status dropdown, add the two values to the API enum, run the app, pick Approved on one record, and watch the badge turn green. Fifteen minutes. They move the ticket and write one word in the channel: Done.

Nothing in that story is lazy. It is how most of us were taught. The problem is the question it never asks:

What did we actually prove?

Proven either way

  • The dropdown shows two new options.
  • One request with APPROVED returned 200.
  • The badge renders green.

Not proven at all

  • The requirement was understoodGate 01
  • The business rules are correctGate 01
  • Invalid transitions are handledGate 03
  • The API behaves under bad inputGate 03
  • Existing features still workGate 04
  • Permissions still workGate 04
  • Reports still count correctlyGate 04
  • The code is maintainableGate 05
  • The change is safe to mergeGate 06

That gap, between “I saw it work” and “we can trust it”, is what this episode is about. The rest of the article closes it with a checklist you can run on every ticket: six gates, each with a question, a lab, and the evidence it must leave behind.

Why a development checklist?

Writing code is one part of software development. Your job is bigger: to create confidence that a change is correct, complete, tested, safe, maintainable and reviewable. Code that runs gives you one of those, partly.

A checklist is not bureaucracy. Pilots and surgeons use them because skilled people under time pressure skip steps they know perfectly well. Developers are no different. The checklist makes the invisible steps visible, so you skip none of them by accident and can explain the ones you skip on purpose.

Don’t just ask: does my code work? Ask six questions instead.

How do I know I am ready? Running the code answers question 2, for the one path you clicked. Click each question to see which gate answers it.

Code works ≠ change is safe0 of 6 answered

The six gates

Each question becomes a gate. A gate is not a meeting or a form; it is a point where you stop and check that you have the evidence to continue. The same structure repeats for every gate below: the question, why it matters, a visual, a checklist, a lab, the usual mistakes, and the evidence to carry forward.

  1. Gate 01Requirement

    Do I understand what I am supposed to build?

    Leaves: rules, criteria, open questions answered

  2. Gate 02Development

    Did I implement it correctly and safely?

    Leaves: the smallest change, a blast radius

  3. Gate 03Unit Test

    Does the changed logic behave correctly in isolation?

    Leaves: tests that fail when the rule breaks

  4. Gate 04Regression Test

    Did my change break something that already worked?

    Leaves: a regression scope, green journeys

  5. Gate 05Self Review

    Would I approve this if someone else wrote it?

    Leaves: a clean, reviewable diff

  6. Gate 06Clearance

    Is there enough evidence to move forward?

    Leaves: CI, review and a complete checklist

Rebuild the order from memory

  1. Gate 01?
  2. Gate 02?
  3. Gate 03?
  4. Gate 04?
  5. Gate 05?
  6. Gate 06?

Which gate comes first?

Gate 01

Requirement

“Do I understand what I am supposed to build?”

The cheapest bug to fix is the one you never write. Most rework does not come from bad code; it comes from good code that solves the wrong problem, or the right problem with a missing rule. Before you open the IDE, turn the ticket into something you could test.

Here is the ticket we will follow through all six gates:

APP-317

Add the ability for an application reviewer to approve or reject a pending application.

  1. AC1Pending application can be approved.
  2. AC2Pending application can be rejected.
  3. AC3Approved application cannot return to Pending.
  4. AC4Rejected application cannot be approved.
  5. AC5Unauthorized users cannot change status.
  6. AC6Status change is visible in the UI.

Read it like an engineer

For every ticket, identify the same nine things. Where the ticket is silent, you have a question, not an assumption.

Functional requirement
A reviewer can move a pending application to Approved or Rejected.
Business rules
Only Pending can change. Approved and Rejected are final. Only reviewers decide.
Inputs
Application id, target status, the signed-in user.
Outputs
The updated application: status, decidedBy, decidedAt.
Error cases
404 unknown id, 400 unknown status, 409 invalid transition, 403 not a reviewer.
Permissions
REVIEWER role only. Applicants can read, not decide.
Existing behaviour
List filters, detail page, dashboard counts and the weekly report all read status.
Affected systems
Next.js UI, NestJS API, PostgreSQL, notification emails, reports.
Acceptance criteria
AC1 to AC6 below. If one cannot be tested, it is not finished.
What should I test? Step through to derive each column from the one before. Then click any rule, criterion or test to trace it back to the ticket.
Column 1 of 5
  1. Ticket1

    • Add the ability for an application reviewer to approve or reject a pending application.

Same ticket, same clock. Press Play. The first developer writes code sooner, and writes it three times.
Tick 1 of 8

Ticket, IDE, codecode written 0x

  1. Ticket
  2. IDE
  3. Code
  4. Review: no permission check
  5. Code again
  6. UAT: rejected can be approved
  7. Code again
  8. Done

Understand, investigate, plancode written 0x

  1. Ticket
  2. Understand
  3. Investigate
  4. Plan
  5. Code
  6. Review: rename one variable
  7. Done

Checklist

0 of 10

Lab: decompose the ticket

Take APP-317 and write, without scrolling up: the functional requirements, the business rules, the acceptance criteria, and at least three edge cases the ticket does not mention. Ten minutes. Then compare.

Common mistakes

  • Coding before understanding the requirement
  • Assuming missing business rules
  • Ignoring edge cases
  • Ignoring permissions
  • Only thinking about the UI
  • Treating acceptance criteria as optional

Evidence before moving on

  • Rules written as testable statements
  • AC1 to AC6 agreed with the product owner
  • Open questions answered, not assumed
  • Affected systems listed

Gate 02

Development

“Did I implement it correctly and safely?”

Development means understanding the existing flow before changing it. A requirement is usually a flow, not a file: the reviewer’s click travels through the UI, the API, a controller, a service and the database, and comes back. Find every stop before you edit any of them.

Then decide where the rule lives. If the “only pending can change” rule sits in a React component, any script with a token can bypass it. Put it in one place on the server, and let everything else call it.

Where does this change travel, and where does it stop? Pick a request and play it. The rule lives in one layer; watch which layer says no.
Hop 1 of 8
  1. User

    Reviewer clicks Approve on the detail page.

  2. 2

    Next.js UI

    Shows the buttons only for pending applications. No rules here.

  3. 3

    NestJS API

    PATCH /applications/:id/status with { status }.

  4. 4

    Controller

    Validates the DTO, reads the user from the guard, calls the service.

  5. 5

    Service

    assertTransition(PENDING, APPROVED, REVIEWER) returns APPROVED.

  6. 6

    Database

    Conditional UPDATE matched 1 row.

  7. 7

    Response

    200 with status APPROVED.

  8. 8

    UI

    Badge shows Approved; buttons disappear.

In flight...

The rule, as a state machine. Anything not drawn is forbidden. Try any move; this runs the real rule.
PENDING
APPROVED finalREJECTED final
From
To
As

200 OK: status is now APPROVED

Implement PATCH /applications/:id/status

  1. 1

    Find the existing UI

    rg -n "ApplicationStatus" apps/web/src

  2. 2

    Find the API endpoint

    rg -n "@Controller(\"applications\")" apps/api/src

  3. 3

    Find the controller

    apps/api/src/applications/applications.controller.ts

  4. 4

    Find the service

    apps/api/src/applications/applications.service.ts

  5. 5

    Find the database model

    rg -n "model Application" apps/api/prisma/schema.prisma

  6. 6

    Identify the business rule

    Write it down: PENDING to APPROVED or REJECTED, nothing else.

  7. 7

    Implement the smallest safe change

    One rule function, one endpoint, two buttons.

  8. 8

    Run the application

    pnpm dev

  9. 9

    Verify the happy path by hand

    Approve one pending application, refresh, status is still Approved.

The rule as data plus one function. Every caller goes through it, so it only exists once.

application-status.ts14 lines
1export type ApplicationStatus = "PENDING" | "APPROVED" | "REJECTED";
2
3export const TRANSITIONS: Record<ApplicationStatus, ApplicationStatus[]> = {
4 PENDING: ["APPROVED", "REJECTED"],
5 APPROVED: [],
6 REJECTED: [],
7};
8
9export function assertTransition(from: ApplicationStatus, to: unknown, role: Role) {
10 if (role !== "REVIEWER") throw new ForbiddenException("only reviewers can change status");
11 if (!isStatus(to)) throw new BadRequestException(`unknown status "${String(to)}"`);
12 if (!TRANSITIONS[from].includes(to)) throw new ConflictException(`cannot move ${from} to ${to}`);
13 return to;
14}

Blast radius

Changing a status field is never only a status field. Before you write a line, list everything that reads it. That list is your blast radius, and in Gate 04 it becomes your regression scope.

What is affected? Play it outward from the change, then click any area to see why it is affected. The further out, the easier it is to forget.
Ring 1 of 3

Depends on it

Reads or writes it directly

Application.status changed

Select an area to see why it is in the blast radius.

Lab: find the flow before you change it

In your own codebase, pick any field your next ticket touches. Find the entry point, the endpoint, the controller, the service and the table. Then run a search for every other reader of that field and write the list down before you start.

investigate3 lines
1rg -n "status" apps/api/src/applications apps/web/src/applications
2rg -n "ApplicationStatus" --type ts
3rg -n "status" apps/api/src/reports apps/api/src/dashboard

Common mistakes

  • Changing too many files
  • Duplicating business logic
  • Modifying unrelated behaviour
  • Ignoring existing architecture
  • Putting business rules inside the UI
  • Not checking downstream effects
  • Implementing before understanding the existing flow

Evidence before moving on

  • The rule lives in one function
  • Endpoint returns 200, 400, 403, 404, 409 as designed
  • Happy path verified by hand, including a refresh
  • Blast radius written down

Gate 03

Unit Test

“Does the changed logic behave correctly in isolation?”

A unit test proves one piece of your logic in isolation, in milliseconds. It is not a test of the feature. It will not tell you whether the button works or the email is sent. It tells you, precisely and repeatably, whether assertTransition says yes and no at the right moments.

Unit test

Tests individual logic or components. Fast, exact, isolated.

Regression and E2E

Tests behaviour across the application. Slower, broader, realistic.

What does a unit test touch? Only the function. Play the seven cases: input goes in, output comes out, and the test asserts on the output. Every case runs the real rule.
Case 1 of 7

it("PENDING to APPROVED")

Input

PENDING, APPROVED, REVIEWER

Function

assertTransition()

Output

returns APPROVED

expect: toBe("APPROVED")pass

Outside the boundary, never touched:PostgreSQLHTTPBrowserEmail service

Which layer should prove this? Many fast unit tests, fewer integration tests, a handful of journeys. Pick the cheapest layer that can truly prove each check by clicking a band.

Check 1 of 6

assertTransition refuses REJECTED to APPROVED

The tests, with Vitest

Seven cases cover the rule: the two allowed moves, every way it must refuse, and the permission. Note that most of the tests are about “no”. That is where the bugs live.

application-status.spec.ts26 lines
1import { describe, expect, it } from "vitest";
2import { assertTransition } from "./application-status";
3
4describe("assertTransition", () => {
5 const allowed = [
6 ["PENDING", "APPROVED"],
7 ["PENDING", "REJECTED"],
8 ] as const;
9 it.each(allowed)("%s can move to %s", (from, to) => {
10 expect(assertTransition(from, to, "REVIEWER")).toBe(to);
11 });
12
13 const blocked = [
14 ["APPROVED", "PENDING"],
15 ["REJECTED", "APPROVED"],
16 ["PENDING", "ARCHIVED"],
17 ["PENDING", undefined],
18 ] as const;
19 it.each(blocked)("%s cannot move to %s", (from, to) => {
20 expect(() => assertTransition(from, to, "REVIEWER")).toThrow();
21 });
22
23 it("an applicant cannot change status", () => {
24 expect(() => assertTransition("PENDING", "APPROVED", "APPLICANT")).toThrow(ForbiddenException);
25 });
26});

A failure should read like a bug report: which rule, which input, what happened instead.

a failing run8 lines
1$ pnpm vitest run application-status.spec.ts
2
3 FAIL assertTransition > REJECTED cannot move to APPROVED
4AssertionError: expected [Function] to throw an error
5 ❯ application-status.spec.ts:21:56
6
7 Test Files 1 failed (1)
8 Tests 1 failed | 6 passed (7)

Lab: break the rule, watch the tests

The runner below executes the real rule in your browser against the seven cases. Run the correct version first, then each bug. A test you have never seen fail has not proven anything yet.

The change: The rule as specified.

$ pnpm vitest run application-status.spec.ts

Pick an implementation, then run the tests. They execute in your browser.

Common mistakes

  • Only testing happy paths
  • Testing implementation details instead of behaviour
  • Writing tests only after something breaks
  • Mocking everything
  • Writing meaningless tests
  • Believing unit tests prove the entire feature works

Evidence before moving on

  • Allowed and refused transitions covered
  • Invalid input and permission covered
  • Each test seen failing once
  • Suite green locally

Gate 04

Regression Test

“Did my change break something that already worked?”

A feature can pass every unit test and still break a journey that worked yesterday. The search page might filter on a status value you renamed. The dashboard might count “everything not pending” as approved. Regression testing asks one question about everything around your change: does it still work?

What existing behaviour touches the thing I changed?

How do I choose what to re-test? Not by running everything blindly, and not by testing only the new screen.
  1. Change

    Application.status gains two transitions.

  2. Impact analysis

    Search the code for every reader of status.

  3. Affected flows

    Detail, list, search, dashboard, reports, emails, permissions.

  4. Regression tests

    The cheapest test that proves each flow still works.

Two journeys with Playwright

Does the journey still work? Play the new journey, then turn the bug on and play again. Only the refresh catches it. Then play the existing search journey: it must still pass.
Step 1 of 7
  1. Login
  2. Open Applications
  3. Open application
  4. Approve
  5. Verify status
  6. Refresh
  7. Status persists

running...

The new journey, ending with a refresh: a status that only lives in client state would vanish here.

approve-application.spec.ts16 lines
1import { expect, test } from "@playwright/test";
2import { seedApplication, loginAs } from "./helpers";
3
4test("reviewer approves a pending application", { tag: "@p0" }, async ({ page, request }) => {
5 const app = await seedApplication(request, { status: "PENDING" });
6 await loginAs(page, "reviewer");
7
8 await page.goto("/applications");
9 await page.getByRole("link", { name: app.reference }).click();
10 await page.getByRole("button", { name: "Approve" }).click();
11 await expect(page.getByTestId("status")).toHaveText("Approved");
12
13 await page.reload();
14 await expect(page.getByTestId("status")).toHaveText("Approved");
15 await expect(page.getByRole("button", { name: "Approve" })).toHaveCount(0);
16});

Lab: define the regression scope

Before looking at the answer: which areas of the Application Management System read the status you just changed? Pick them, then check. On your own tickets, ask the same question of every screen, endpoint, job and report.

This lab builds on Gate 02: Development. Finish it first, then come back.

Go to Gate 02

Select every area that reads or writes Application.status, then check.

0 of 8

Common mistakes

  • “My unit tests passed, so I’m done.”
  • Only testing the new feature
  • Testing only the changed screen
  • Ignoring related APIs
  • Ignoring permissions, reports, search and filters
  • Running the entire suite without understanding what matters

Evidence before moving on

  • A written regression scope
  • New journey passes, including after refresh
  • Existing journeys pass
  • Gaps recorded as tickets

Gate 05

Self Review

“Would I approve this if someone else wrote it?”

Before anyone else sees your change, review it the way you would review a stranger’s. Your reviewer has less context than you and less time. Every debug log, stray file and copy-pasted branch they find is one they should not have had to find.

The tool is git diff. Read every line in it: files changed, lines changed, anything unintended, debug logs, commented-out code, duplicated logic, naming, architecture, security and performance.

What do I hand to the reviewer? Not your working tree. A changeset you have already read.
  1. Code

  2. git diff

    Read it all

  3. Self review

    Four lenses

  4. Clean changeset

  5. PR

The commands

before every PR5 lines
1git status # which files changed, any you did not expect?
2git diff --stat # how big is this change, really?
3git diff # read every line as a reviewer would
4git diff --staged # what will actually be committed
5rg -n "console.log|debugger|TODO" $(git diff --name-only)

Code

0 of 6

Security

0 of 4

Performance

0 of 4

Maintainability

0 of 4

Lab: review this diff

This change “works”: a reviewer can approve a pending application. Read the diff and write down every problem before revealing the findings. There are seven.

PR #482: git diff19 lines
1diff --git a/apps/api/src/applications/applications.service.ts
2@@ async changeStatus(id: string, to: string, user: AuthUser) {
3+ console.log("changeStatus", id, to, user);
4+ const app = await this.prisma.application.findUnique({ where: { id } });
5+ if (to === "APPROVED" && app.status === "PENDING") {
6+ return this.prisma.application.update({ where: { id }, data: { status: to } });
7+ }
8+ if (to === "REJECTED" && app.status === "PENDING") {
9+ return this.prisma.application.update({ where: { id }, data: { status: to } });
10+ }
11+ // if (user.role !== "REVIEWER") throw new ForbiddenException();
12+ return app;
13diff --git a/apps/web/src/applications/StatusActions.tsx
14+ const canDecide = app.status === "PENDING" && user.role === "REVIEWER";
15diff --git a/apps/web/src/applications/StatusBadge.tsx
16- const label = status.charAt(0) + status.slice(1).toLowerCase();
17+ const label = status[0] + status.substring(1).toLowerCase();
18diff --git a/.env.example
19+DATABASE_URL=postgres://admin:Pa55word@prod-db.internal:5432/apps

Common mistakes

  • Immediately opening the PR
  • Assuming the reviewer will find everything
  • Reviewing only functionality
  • Ignoring security and performance
  • Leaving unrelated changes in the PR

Evidence before moving on

  • Every changed line read
  • No debug or commented-out code
  • No unrelated files
  • Security and performance considered

Gate 06

Clearance

“Is there enough evidence to move forward?”

Clearance is not “someone clicked Approve”. It means the change has passed every quality gate the team requires, and the evidence for each one exists where someone else can see it: in the PR, in CI, in the review thread.

How does a change earn clearance? Play it. Every stop adds evidence; CI turns six claims into facts on a clean machine.
Stop 1 of 10
  1. Developer
  2. Self review
  3. Unit test
  4. Regression test
  5. PR
  6. CI
  7. Code review
  8. Approval
  9. Clearance
  10. Ready for release

CI checks

  • lint
  • typecheck
  • unit tests
  • build
  • integration tests
  • e2e tests

CI turns most of those inputs into facts on a clean machine. A typical pipeline runs: lint, typecheck, unit tests, build, integration tests, e2e tests.

Clearance checklist

0 of 13

Lab: would you clear this PR?

This lab builds on Gate 03: Unit Test and Gate 05: Self Review. Finish those first, then come back.

Go to Gate 03

#482 feat/application-decisions

Approve and reject pending applications

The author says it works on their machine and a teammate already clicked Approve. Would you clear this PR?

  • Requirementno evidence
  • Unit testevidence
  • Regressionno evidence
  • Code reviewno evidence
  • CIno evidence
  • Self reviewno evidence
Clearance5 of 6 inputs missing
Not ready
  1. Failing test

    blocks: CI

    CI: unit tests failed. REJECTED cannot move to APPROVED.

  2. Missing regression test

    blocks: Regression

    No Playwright test covers approve then refresh. Search journey not run.

  3. Debug console.log

    blocks: Self review

    applications.service.ts line 12 logs the user object.

  4. Incorrect permission handling

    blocks: Requirement

    The role check exists only in the UI. AC5 is not met: an applicant can call the API.

  5. Unrelated file modification

    blocks: Code review

    StatusBadge.tsx refactor and a .env.example change are not part of this ticket.

Common mistakes

  • Treating an approval as clearance
  • Merging with a red check “because it is flaky”
  • Resolving comments without answering them
  • Skipping the checklist on small changes

Evidence before moving on

  • All CI checks green
  • Review approved, no blocking comments
  • Every checklist line has a link or a note
  • Ready for deployment

The full lab: all six gates, one feature

Now run the whole checklist once, end to end, on the Application Management System. Seventeen steps; each shows the action, why it matters, the command or code, the expected result, and a checklist.

Frontend
Next.js, TypeScript, Vitest, Playwright
Backend
NestJS, TypeScript, Vitest
Database
PostgreSQL

This lab builds on Gate 06: Clearance. Finish it first, then come back.

Go to Gate 06

Step 1 of 17, Requirement

Read the ticket

Why
Everything later is measured against these sentences. Read it twice, once as the user and once as the tester.
Command or code
requirement1 lines
1APP-317 Application reviewers can approve or reject pending applications.
Expected result
You can say the change in one sentence without reading it again.
Checklist
  • Who is the user?
  • What do they do?
  • What changes for them?

Definition of done

“Done” means two different things, and most arguments about quality are really arguments about which one someone meant.

Code complete ≠ work complete

Most tickets stop at one piece. Tick what you would have evidence for on your last ticket.

Code complete. 7 pieces of evidence missing.

Take the checklist with you

Paste it into a ticket or a PR description. Tick a line only when you can point at the evidence.

01Requirement

0 of 5

02Development

0 of 4

03Unit Test

0 of 5

04Regression

0 of 6

05Self Review

0 of 7

06Clearance

0 of 8

Closing

Before you say a ticket is done, don’t ask whether you finished coding.

Ask whether you have enough evidence to trust the change.