Free

3,000 Lines in One Commit: Why Complex Features Need Plan Mode First

The instinct with complex features is "let me just try something." But architectural decisions hide in the 3rd model, the 5th edge case. Plan mode's real value is moving the architecture-level conversation to text before code. Real case: Pickful's community jury system — 3,032 lines, 119 specs green, one commit, zero architectural rework after.


The instinct with complex features is "let me just try something."

The problem: complex systems have architectural decisions hidden in the 3rd model, the 5th edge case, the 8th points rule. Diving into code and hitting those decisions later — changing them then costs 10× what it would have cost to talk them out in text first.

I used Claude Code's plan mode to build TopicReview, the community jury system in Pickful, and got to see the extreme version of this inequality: one commit, 3,032 lines, 119 specs green, shipped in a single go. None of the dozen-plus commits that followed was an architectural redo — they're all parameter tuning, UI polish, edge-case patching. The system has been running steadily ever since and is now central to how the community self-moderates.

This post is about why complex features need plan mode first, what the plan phase is actually doing, and when the conversation is done enough to start writing code.

How Complex the System Is

TopicReview is a community vote to decide whether a low-quality post gets removed. One sentence to state — but the spec unfolds in layers:

  • 5 states: open → voting → decided → appealed → closed
  • 3 verdicts: remove / warn / keep
  • 2 phases: initial review by 12 jurors; appeal by 5 judges (drawn from the top-20 users by points)
  • Multi-dimensional points flow: 10 pt provisional penalty, 10 pt appeal stake, +5 for jurors who voted with the verdict, +10 for judges who voted with the verdict, +3 for reporters whose report was vindicated, appeal-won refund = stake + bonus + provisional penalty
  • 4 kinds of scheduled jobs: 24h initial voting window, 24h appeal window, 24h appeal review window, and — juror points deferred 24h after a remove verdict (since an appeal might overturn it)
  • Parallel flows + rollback: if votes flip during provisional removal, the post must be restored and the provisional penalty refunded; if the appeal overturns the verdict, the stake is refunded plus a bonus, possibly with the provisional penalty too, and jurors get re-scored by the new verdict

The complexity is not in any single rule — it's in how the rules interact. Every rule you add can trigger a rollback in another.

Walls You Hit Without Plan

When you write straight through, the hardest problems aren't the visible facts — they're the questions you never thought to ask. Read TopicReview's code backward, and at least four walls would inevitably hit you if you skipped the plan:

Juror eligibility rules. Looks like just User.jurors_and_judges.sample(12). But the actual rules are: exclude the post author, exclude anyone who reported the post, exclude anyone who already voted in the initial round (so they can't also vote on appeal). Three layered exclusions. Writing the model all in one go, you'll miss one or two.

Deferred points payout. A remove verdict would normally trigger juror payouts. But an appeal can overturn a remove — and when it does, the jurors who voted with the old verdict are now on the wrong side, so payouts need to be recomputed against the new verdict. Therefore the remove verdict must wait for the 24h appeal window to close before paying jurors. Skip writing that rule down, pay first, and you end up trying to claw back points — about 10× messier than paying late.

Scheduled-job interface design. CloseTopicReviewJob looks like "finish a review." In practice it handles three situations:

# Initial voting window expiration
CloseTopicReviewJob.set(wait_until: voting_ends_at).perform_later(review.id)
# Deferred juror payout after a remove verdict
CloseTopicReviewJob.set(wait: 24.hours).perform_later(review.id, award_juror_points: true)
# Appeal window expiration
CloseTopicReviewJob.set(wait: 24.hours).perform_later(review.id, appeal_id: appeal.id)

No upfront planning and you'll write the first signature, then realize at case 2 that the interface has to change.

The most expensive wall: can you appeal during provisional removal? Once remove votes cross the threshold, the post is hidden (provisional), but the whole review is still in the voting state — not decided. Can the user appeal yet?

  • Yes → you have to expand the transitions between decided and appealed
  • No → UX suffers: the post is down and the user has to wait 24h before they can even protest
  • TopicReview's actual choice: yes, appeal is allowed during provisional removal — but the review has to first finalize through the decided transition before appeal opens

That single decision reaches back and changes the parameter checks inside open_appeal! and the state-machine logic. Deciding halfway through = rewriting half the system.

What the Plan Phase Is Actually Doing

Claude Code's plan mode is a mode where writing code is disallowed — Claude can read the repo, think through approaches, discuss with you, but every file modification is hard-blocked until you approve a plan.

That mechanical constraint is the point: it forces the architecture-level conversation to happen in text.

A few things the plan phase is doing in practice:

1. Drawing the state machine and the roles. 5 states, 4 roles (juror / judge / post author / reporter), what each role can and can't do at each state — all in a few lines of markdown. A few lines vs. tens of files. The cost of change is two orders of magnitude apart.

2. Walking through each role's flow:

  • Juror's view: gets notified → opens the review → reads the post + the reason → votes with a reasoning → gets points
  • Post author's view: gets notified → sees verdict → if remove, considers appeal → puts up the stake → waits
  • Judge's view: gets the top-20-drawn appeal → votes → gets points

Wherever the walk stalls, a hidden question pops out: "can jurors see each other's votes?" "what can the post author do during provisional removal?"

3. Interrogating edge cases. The plan phase is not designing the "normal path" — it's deliberately asking the questions that don't usually surface:

  • What if 0 votes come in by deadline? (Final call: default to keep.)
  • What if the appeal gets 0 votes by deadline? (dismissed; stake forfeited.)
  • Can a judge vote on a post they themselves reported? (No — same exclusion rule.)
  • Multiple jurors trigger finalize at the same time — does the race double-pay? (Add a juror_points_awarded field for idempotency.)

95% of these questions never surface naturally while writing code. The plan phase forces you to answer them one by one.

4. The points ledger. The points system is complex enough that talking isn't enough — you have to actually draw the table: each point flow with its trigger, amount, and rollback path. Once the ledger balances, every edge case (provisional refund, appeal refund, bonuses) reconciles.

How Done Is Done Enough to Start Coding

A hard test:

  • You can walk every path without stalling — every combination from opening the review to closed (keep / remove / warn × appealed / not × provisional / not) — you can talk through it end to end
  • Every edge case has a defined behavior — not "we'll see," but "0 votes defaults to keep," "provisional allows appeal but must finalize first"
  • There's no "oh, haven't thought about that yet" — every question you can formulate already has an answer

Once that bar is met, writing code becomes translating the plan into Ruby.

What the Straight-Through Execution Phase Looks Like

The first ship of this system:

d162f1e Add community moderation system with jury/judge review and appeals
  63 files changed, 3032 insertions(+)
  119 specs, 0 failures

63 files, 3,032 lines, 119 specs green. Shipped in one go.

The dozen-plus commits after prove the architecture held:

Apply legacy penalty (1pt) for posts created before 2026-03-26
Fix topic review appeal bugs: window mismatch and verdict not updated
Add topic_removed status for posts removed by community review
Default to keep verdict when review expires with zero votes
Reduce provisional removal penalty to 1pt during trial period
Allow topic creator to withdraw active reviews
Add handled tab to jury dashboard, fix tabs styling

Every one is tune / fix / polish / a small feature. Not one is "go back and redesign the state machine" or "the points model needs to change." The skeleton held.

This is the real payoff of plan: execution stays uninterrupted. No "wait, how does this path work" — the plan already covered it. No "I didn't think of this edge case" — the plan phase asked. No "this interface has to be redesigned" — the plan already settled it.

Hours of writing code, doing only one thing: translating a clear design onto the keyboard.

When You Shouldn't Use Plan

Not every task is worth planning:

  • Simple bug fixes — locate + fix + add regression test; no discussion needed
  • Mechanical refactors — rename, extract, relocate; the path is unique, plan is one extra step
  • There's only one obvious path — adding a GET endpoint, a UI toggle; nothing to debate
  • Exploratory prototypes — you don't know yet what you want; running something rough beats thinking more

Plan mode's payoff comes from lowering rewrite cost. If there's no rewrite risk in the task, plan is just overhead.

Closing

"Think before you act" sounds like a personality tip. Plan mode isn't that.

Plan mode is about making the architecture-level conversation happen where 10 words can change it, not where 30 files have to change.

Under complexity, that old industry saw "words are cheap, code is expensive" needs to be flipped — it doesn't mean "just write code first and see" (which only holds when the cost gap is small). It means using the cost gap: put the expensive conversation up front, in the cheap medium.

3,000 lines in one commit feels great. Not because I wrote fast — because the plan turned "writing" into a purely mechanical act.