Why I Rewrote Countdown
Joel Spolsky has a famous essay called “Things You Should Never Do, Part I” where he argues that rewriting software from scratch is the single worst strategic mistake a company can make. You throw away years of bug fixes. You underestimate the complexity of the parts you don’t remember. You deliver nothing for a year.
In March 2026 I threw away my cooperative card game and rewrote it from scratch. Seventeen days later I had something I was proud of.
This is the story of why that worked — and why it works less often than you think.
What v1 was
The first Countdown was a Flutter-only implementation of a cooperative card game in the vein of The Mind. Players try to play cards in descending order, silently, without signaling. It was playable. It had the game loop. It had a UI. It also had problems I didn’t know how to fix without pulling everything apart.
The biggest one was that game logic lived inside the Flutter app. That meant:
- The server was an afterthought, or there wasn’t one.
- Porting to other platforms meant re-implementing rules in parallel.
- The “source of truth” for what the game was lived inside a widget tree.
You can ship a game this way. You can’t evolve one this way. Every change touches everything, because nothing is separated from anything.
The Spolsky test
Spolsky’s argument applies most strongly to software that’s been in production for years, has thousands of edge-case fixes invisible in the code, and serves real users. Countdown v1 was zero-for-three. It had been in development for a few weeks. Its bug fixes were recent and recoverable. Nobody was playing it.
The other half of the Spolsky rule is underestimating the rewrite. A rewrite is cheap when:
- The scope is small enough that you can hold the whole thing in your head.
- The lessons from v1 are clear, and they’re architectural rather than cosmetic.
- You have a working v1 to reference for the tricky parts.
- You have the velocity to do it in days, not months.
Countdown passed four-for-four on rewrite economics. So I rewrote it.
The plan: three phases, each testable
The hardest thing about a rewrite isn’t the code. It’s sequencing. You can’t ship a pure game engine — nobody plays a pure game engine. You can’t ship just a server either. The thing that’s playable is the full stack, which means the full stack has to come up together.
My answer was to build in three phases, each independently testable:
Phase 1: Pure Dart game engine. No Flutter, no sockets, no I/O. Just the rules of the game expressed as deterministic functions on immutable state. A console bot simulator to exercise it. A baseline suite of unit tests covering deck, hand, player, engine, play results. If the engine is wrong, everything downstream is wrong. So the engine went first.
Phase 2: WebSocket server wrapping the engine. The server is a thin wrapper over the engine, adding rooms, player ↔ engine ID mapping, message routing, and broadcast logic. More server-side tests on top of a fake sink that records what was sent. A room test doesn’t need a real WebSocket — it needs a boundary you can drive.
Phase 3: Flutter client. A dumb client that renders server state and sends player intents. No game logic on the client. Widget tests on top. The client gets its state from the server, not from its own reducers.
The point of this phasing was independence. Each phase had a coherent test suite, a clear contract, and could evolve separately. It also meant I was never more than a day away from something that worked end-to-end, because phase 1 is a playable game with bots — via the console — before phase 2 is even written.
The architectural insight that made it worth doing
The single most important decision was putting the game engine in its own package — countdown_core — and importing it as a path dependency from both the server and the client. The server imports it for logic and types. The client imports it for types only. The rules exist once, in pure Dart, deterministic and testable in isolation.
This is the lesson v1 taught me. The game is the engine. The transport is a detail. The UI is a projection. When those three things live in the same codebase undifferentiated, the game is nowhere and the code is everywhere.
In v2, if I want to port to a native iOS SwiftUI client, the engine comes with me. If I want to build an AI that plays the game, it runs against the engine directly. If I want to write a deterministic replay test, I feed the engine a sequence of moves and check the output. I can do things in v2 that were architecturally impossible in v1 — and I didn’t have to invent any of it. I just had to start from the right place.
Seventeen days
The velocity number is more interesting than I expected. Averaging a dozen-plus commits a day for seventeen days is not a pace I could have maintained in 2022. It’s the product of three things:
- Scope discipline. I knew what I was building. The rewrite wasn’t a “let me reconsider the design” project. It was “execute the design I already learned from v1.”
- Test-first structure. Every phase shipped with its tests. When I hit a confusing bug two weeks in, the test suite told me where to look. I was never spelunking.
- AI collaboration at velocity. Roughly a fifth of the commits are co-authored with Claude. The split was: I wrote the architecture and most of the tests; Claude wrote a lot of the implementation. I spent my energy on structural thinking, Claude spent its cycles on typing, and the project didn’t stall in the “I know what I want but I don’t feel like writing it” valley where my side projects usually die.
On that last point: the Spolsky essay is from 2000. He didn’t have a tireless pair programmer who would cheerfully write the boring half of a rewrite. The economics of “throwing away code” shift when the labor of writing replacement code is structurally cheaper. I don’t think Spolsky is wrong. I think the threshold at which rewriting is reasonable has moved, and moved a lot.
When to rewrite, when to retrofit
I’m not writing this to sell rewrites. I’m writing it because I had to make the call and I want to show my work.
Here’s my rule of thumb after v2:
Rewrite when the scope fits in your head, the lessons are architectural rather than cosmetic, you have a working v1 to reference, and your velocity is high enough that the rewrite lands in days or weeks. Bonus: you have one design decision that a rewrite unlocks and no retrofit can reach.
Retrofit when users are on the old version, the codebase carries non-obvious invariants, the lessons from v1 are “we’d do this differently” rather than “this design is wrong,” or the estimated rewrite is measured in months. Most of the time, this is the right call. That’s why Spolsky wrote the essay.
Countdown v2 passed the rewrite test. Most projects don’t. The mistake isn’t to rewrite when it’s right. It’s to call it right when it isn’t. Be honest about which one you’re doing.
A word of caution
Nothing about this generalizes if the project is larger, has users, or has hidden complexity. The Spolsky rule is the default for a reason. I’m writing a card game, not replacing a payment system. If you’re thinking about rewriting a production system, the bar is much, much higher than “I have cleaner ideas now.”
But if you’re thinking about rewriting a side project that’s been sitting in the same shape for a year because the shape is wrong, and you can see the right shape, and it fits in your head: go.