Review as Design Feedback — and Why It Arrives Too Late
By the time a change reaches review the boundary already exists. That is why "this belongs somewhere else" is the comment most often agreed with and least often acted on.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
Why does a code review almost never move a boundary, even when the reviewer is right and the author agrees?
A reviewer opens an 800-line pull request and sees that VAT is being computed inside the checkout handler, while the discount rule lives in a pricing module. She is certain it belongs in pricing. The feature is due Thursday.
Code review is where design gets checked. A good reviewer catches structural problems, so raising this at review is exactly the process working as intended.
It works right up until the objection is expensive, which is precisely when it matters. Cheap objections get acted on; structural ones arrive attached to a week of rework and a Thursday deadline.
- It works right up until the objection is expensive, which is precisely when it matters. Cheap objections get acted on; structural ones arrive attached to a week of rework and a Thursday deadline.
- The economics are backwards. The comment costs one minute to write and a week to act on, and the person writing it pays neither — which is a recipe for objections that are correct and ignored.
- Sunk cost is not a fallacy here, it is arithmetic. The code exists; redoing it is genuinely more expensive than not redoing it, and "we agreed you were right, I made a ticket" is the rational outcome rather than a moral failing.
- Momentum makes it worse over time. The longer the branch, the more code sits on top of the questioned decision, so the objection's cost grows while the reviewer's ability to notice it stays flat.
- And review has almost no way to see the alternative. The reviewer sees what was built, not the three designs the author considered and rejected, so raising a boundary question means asking the author to re-derive a decision from scratch in a comment thread (Design Review).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- A week of work already exists, and it is built on top of the decision being questioned.
- The reviewer cannot make the change herself; she can only ask for it.
- The deadline is real, and the person who would pay for the rework is the author, not the reviewer.
- Nobody wrote down that the decision was made, so from the author's side this arrives as new information about their finished work.
- A design objection that is agreed with and not acted on must leave a record, or the codebase now contains a known-bad structure that nobody can find (The Debt Register).
- Whatever is merged is what the next reader will treat as the intended design, whatever the thread says.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Design review, before code, owns boundary decisions. That is the only place where moving one is cheap (Design Review).
- Code review owns everything downstream of the boundary: behaviour, invariants, failure modes, tests, observability (A Review Checklist Worth Reading).
- The author owns naming the boundary decision before building on it — a paragraph in a ticket, a sketch, a message in a channel. This is the cheapest intervention available and almost nobody does it.
- Whoever decides not to act on an agreed objection owns recording it, with what it will cost and what would trigger revisiting (Revisit Triggers).
- The feedback boundary is the moment the boundary exists in code. Before it, feedback costs a conversation; after it, feedback costs the code plus everything built on it.
- The escalation boundary is deployability: an objection that can be addressed behind the existing interface can wait, and one that changes a published contract cannot (Versioned Interfaces).
- The record boundary is the merge. Anything not written down before merge exists only in a thread that the next reader will not open.
The same comment, at two different times
The reviewer is right in both columns and the objection is word-for-word identical. Everything that differs is when it was said, and that difference decides whether it gets acted on.
This is the device this domain uses to argue about structure, turned on the review process itself: one change, priced two ways.
VAT is computed inside CheckoutController. The discount rule already lives in a Pricing module. A reviewer says tax belongs there too.
The author would have to unpick a week of work with the feature due Thursday. What actually happens: a thread agreeing the reviewer is right, a follow-up ticket, and a merge. The ticket is still open a year later, and the next tax change touches four modules (Shotgun Surgery).
The comment costs a sentence and a redrawn box. Nobody is defending work they have already done, which is most of the reason the conversation goes differently — the author has no sunk cost to weigh and the reviewer has no deadline to be responsible for.
The window closes fast
The cost of acting on a boundary objection rises steeply with how much code sits on top of it, and the probability of it being acted on falls at the same rate. The two curves cross somewhere around the first slice.
The last column is the one worth reading, because it describes what teams actually do rather than what the process says.
- Rows two and three are the entire window. Almost every design objection worth acting on has to arrive in one of them.
- Row four is not a process failure — given the costs in that row, the ticket is the rational choice. The failure is that the objection had nowhere earlier to go.
- The tell that a team is stuck in row four: its reviewers have stopped making structural comments, because they have watched three of them turn into tickets (Tone, Disagreement and Receiving Review).
| When the boundary objection is made | What acting on it costs | What usually happens |
|---|---|---|
| While the requirement is being written | A conversation | The design is simply different, and nobody remembers a decision was taken (Requirements Before Design) |
| On a one-page sketch, before code | A redrawn box | Acted on. This is the cheap window and it is short |
| On the first slice of the implementation | A day, perhaps two | Usually acted on, if nothing has been built on top of it yet (Review Size) |
| On the finished pull request | The rework, plus everything downstream, plus a rebase | Agreement, a follow-up ticket, a merge. The ticket does not get done |
| After merge, in a retrospective | A migration with compatibility windows | It becomes a known-bad structure everyone routes around (Incremental Migration) |
| Two years later, when it blocks a feature | A project | Someone finds the original review thread and the team learns nothing it did not already know (Interest: Why Debt Compounds) |
Moving the feedback earlier
The fix is not a heavier process. It is one artefact — a page — for the subset of changes that create or move a boundary, plus slicing so the boundary lands first.
Each step has a way of degrading into ceremony, and the failure column is where most attempts at this actually end up.
- 1Write the requirement down
Gives the reviewer, and the author, something to compare the behaviour against (Requirements Before Design).
fails by A one-line ticket, after which the author decides what the feature is and the review has no reference point.
- 2Decide whether this needs a sketch
Asks one question: does this create or move a boundary? If not, skip everything below and just build it.
fails by Becoming mandatory for everything, which is how the practice dies within a quarter (When Design Does Not Pay).
- 3Sketch it, one page
Names which module owns the new knowledge, which invariants move, what the interface is, and what can fail (A Feature Design Template).
fails by Four pages, arriving with the code, read by nobody.
- 4Review the sketch, not the code
Puts the boundary objection in the window where it costs a redrawn box.
fails by The same two people approving it in a minute, which is a formality with a document attached (Design Review).
- 5Slice so the boundary lands first
Puts the structural decision in a small, early pull request where an objection still costs a day (Slicing a Feature).
fails by Slicing by file rather than by behaviour, so the boundary is spread across all six slices and visible in none.
- 6Review each slice for what review is good at
Behaviour against requirement, invariants, failure modes, tests, observability (A Review Checklist Worth Reading).
fails by Re-opening the boundary question on slice five, where it is expensive again and was already decided.
- 7Record what was decided, and what was deferred
A short note on the boundary chosen and why, plus a debt entry with a trigger for anything knowingly left wrong (Architecture Decision Records).
fails by Written after the fact and describing what was built rather than what was decided, which is a description rather than a record (Documentation Decay).
Steps two and five carry the weight. Without the threshold in step two the practice becomes bureaucracy and is abandoned; without the slicing in step five the design feedback still arrives attached to a week of work, sketch or no sketch.
How to build it
Most important first.
- Move the boundary question upstream. A one-page sketch that says which module owns the new knowledge, which invariants move, and what the interface is, reviewed before any code, is where a boundary objection costs a redrawn box (A Feature Design Template).
- Slice the implementation so the boundary lands in the first small pull request. Then a boundary objection arrives when a day of work exists, not a week (Review Size).
- At review time, ask a different question: not "is this the right boundary" but "what does this boundary cost us, and can we contain it". Sometimes the answer is a seam that makes the move cheap later, which is a fraction of the price of moving it now (Seams).
- When the objection is right and rework is genuinely too expensive, record it as debt with the cost and the trigger — never as a follow-up ticket in a backlog nobody reads (Deliberate Debt).
- Write the decision down when it is made, not when it is questioned. "Tax lives in the handler for now because pricing has no country context; revisit when we add a second tax regime" turns a review argument into a review acknowledgement (Architecture Decision Records).
- Accept that some design feedback at review is worth acting on anyway. The test is whether the boundary is about to become load-bearing: a wrong boundary that three more features will build on is worth a week now (Change Amplification).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Objection raised on a sketch: cost is a paragraph and a redrawn diagram. The next change lands in the right module and costs one edit.
- Objection raised on the first slice: cost is a day. Still usually worth paying, and this is the last cheap window.
- Objection raised on the finished pull request: cost is the rework plus everything built on top plus a rebase against a week of main. What actually happens is a ticket, so the real cost is deferred and paid by whoever makes the next pricing change — which is the classic shape of interest on debt (Interest: Why Debt Compounds).
- Objection raised after merge: cost is now a migration, because other code depends on the shape (Incremental Migration).
- What does not get cheaper by moving upstream: a sketch cannot discover what only writing the code reveals — that the payment provider has no idempotency key, or that the pricing module has no country context. Some boundary decisions are genuinely not knowable until implementation, and for those, review is the first honest opportunity.
- A design step costs calendar time on every feature that goes through it, including the majority where the design was obvious and the sketch was ceremony. That cost is immediate and the benefit is a rework that did not happen, which nobody experiences.
- Sketches are confidently wrong in ways only code reveals. Reviewing one can produce a worse decision made more formally, and with more authority behind it.
- Front-loading design pushes a team toward deciding things before they have to, which is in direct tension with keeping decisions reversible and deferring commitment (Reversible and Irreversible Decisions).
What can go wrong
- The follow-up ticket. Agreed, filed, never done, and the codebase now contains a structure whose own author would tell you it is wrong. This is the single most common outcome of a design comment on a finished pull request.
- The reviewer stops raising it. After the third correct-and-ignored objection, the rational response is to stop making them, and the team loses design review entirely without any decision being taken.
- The rework happens, the deadline is missed, and the lesson the team draws is "do not raise design in review" rather than "raise design earlier".
- The mitigation fails: design review is introduced, becomes a mandatory document for every change including trivial ones, and is abandoned within a quarter as bureaucracy (When Design Does Not Pay).
- A sketch is reviewed and approved, then the implementation diverges from it, and nobody compares the two — so the design review provided assurance rather than feedback.
- This depends on there being a design step at all. Where there is not, the pull request is the first artefact anyone can respond to and review inherits a job it cannot do.
- It depends on slicing being possible, which depends on flags and expand-and-contract migrations (Expand and Contract).
- It depends on a debt record that is actually consulted. Without one, "we agreed and deferred" is indistinguishable from "nobody noticed" six months later (The Debt Register).
- "So do not raise design problems in review." Raise them — the record matters, and occasionally the boundary is about to become load-bearing and the rework is worth it. The point is to stop expecting review to be where design happens.
- "So we need a design document for every change." That is the over-correction, and it dies within a quarter. The threshold is whether the change creates or moves a boundary (When Design Does Not Pay).
- "Sunk cost is a fallacy, the rework should just happen." The fallacy is counting already-spent effort as a reason. The remaining cost of rework is real, and on a Thursday deadline it is often correctly decisive.
- "A follow-up ticket resolves it." A ticket in a backlog is an intention. A debt record attached to the code, with a cost and a trigger, is a decision (The Debt Register).
- divergent-change
Testing it, and how it ages
- The audit that tells you whether this is happening: search merged pull requests for threads where the reviewer raised a structural point and the resolution was a ticket. Then check how many of those tickets were closed.
- For design sketches, the test is divergence — compare the merged structure against the sketch. Repeated divergence means the sketch is being written to satisfy a process rather than to think (Documentation Decay).
- For any deferred objection, the test is whether the record is findable from the code it describes. A debt entry in a wiki that the module does not point at will not be found (Docs Close to Code).
- Teams typically discover this in one direction: they add design review after an expensive structural mistake, over-apply it to everything, then settle on applying it to changes that create or move a boundary and nothing else. The settled version is the right one and takes about a year to reach.
- As a system grows, more changes become boundary-crossing, so the fraction needing a design step rises. A team that fixed the threshold early will find it drifting out of date silently.
- What eventually forces the issue: a structure everyone agreed was wrong at review time becomes the thing blocking a major feature two years later, and someone finds the thread.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- GENERALThe economics — a comment costing a minute to write and a week to act on, paid by different people — are a property of reviewing finished work, so they hold on any team that reviews pull requests, whatever the stack.
- SCALE-SPECIFICOn a team of three, the design conversation happens before the code without any process, because everyone hears about the work as it starts. At thirty across time zones the first anyone hears is the pull request, which is why the explicit design step is a scaling artefact rather than a maturity one — adding it to a team of three is usually pure overhead.
- CONTESTEDThe strongest opposing view is that upfront design review is where the worst decisions get made, because they are made by the people with the most authority and the least information — before anyone has written a line — and then defended because they were formally approved. On this view the pull request is the *right* place for design feedback precisely because a working implementation exists to argue about, and the correct fix for expensive rework is smaller pull requests, not earlier documents. That position has a strong track record on teams with genuinely small changes; it degrades badly where a boundary decision is only visible once a week of work has already committed to it.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — boundary decisions with system-wide consequences need a forum sized for them; a pull request thread is the wrong medium and an RFC is the usual right one (RFCs).
- — Testing & Reliability Engineering — the reason slicing works at all is that each slice is independently verifiable, which is a testing property before it is a review one.