18 MAR 2026

Fixes Are Bugs

Last week my partner told me he was paying $3,600 a month for HubSpot. Rated 1.8 out of 5 on Trustpilot. He couldn't tell which marketing channels actually produced revenue. Events lived in Airtable. Contacts lived in HubSpot. The connection between "we sponsored this conference" and "that conference produced three deals" didn't exist.

So I built a replacement. Full CRM with funnel attribution as the core feature, not a bolt-on. Six phases: auth, contacts and deals, attribution and forms, events, reporting, command center. One day.

The features worked. Every module passed its tests. The visitor tracking captured UTM parameters. The attribution engine connected form submissions to marketing channels. The deal pipeline moved cards between stages. The reports showed revenue by channel.

Then I pushed it for code review.


The automated reviewer came back with a score of 2 out of 5.

Fair enough. It found real issues. HTML injection in the invite email template. A race condition in the contact upsert — two form submissions from the same email could create duplicate records. The event DELETE endpoint didn't scope to the current team, so any authenticated user could delete any team's events. Real bugs, clearly worth fixing.

I fixed them all. Pushed again.

Score: 3 out of 5.

New issues. The ROI attribution query joined attendees to deals but didn't account for attendees who were contacts before they attended the event. The pipeline stage rename didn't migrate existing deals to the new slug. The duplicate prevention fix used onConflictDoUpdate, which silently updated existing contacts even when the conflict came from a different team's data.

I fixed those too. Each fix was correct in isolation. Each fix touched 2-3 files. Each fix changed the system just enough to create new surface area for the reviewer to find problems.

Score: 4 out of 5. Then 2 again. Then 3. Then 4. Then 3. Then 4.

Eight rounds total. The score didn't converge. It oscillated.


Here's what was happening. The pipeline DELETE orphan fix forgot stage remapping. The onConflictDoUpdate race fix created an isNewContact flag that wasn't checked downstream. The attendee status validation fix broke the ROI query because it changed which attendees were counted. The stage slug migration used name-based matching, which failed when two stages had similar names.

Every fix was a correct response to a real problem. And every fix introduced a new problem that didn't exist before the fix.

This isn't a quality problem. It's a physics problem. In a system where modules depend on each other, changing one module changes the behavior of everything that touches it. The fix for Module A's bug becomes Module B's bug. You're not converging on zero bugs. You're playing whack-a-mole in a graph.


I wrote about the integration tax a few days ago — the idea that connections between modules are where projects die, not the modules themselves. This was the integration tax in action, but with an extra twist: the review process itself was generating integration debt.

The reviewer would flag a boundary issue. I'd fix the boundary. The fix would shift behavior at a different boundary. The reviewer would flag the new boundary issue. I'd fix that one. The cycle continued because each fix was local and the system is global.

What broke the cycle was changing my approach. Instead of fixing each review comment in isolation, I started tracing the full data flow before writing any code. Pick up a flagged issue. Read the file. Read every file that calls into it. Read every file it calls out to. Understand the full path from user action to database write to API response. Then — only then — write the fix.

The traces caught things the reviewer missed. The reviewer flagged that the event DELETE wasn't scoped to the team. The trace revealed that the event CREATE also wasn't scoped correctly — it would let you create an event for a team you didn't belong to. Same class of bug, different endpoint, not flagged because the reviewer was looking at the diff, not the system.


After the sixth round I built a tool. Three parallel analysis agents, each with a specific checklist. One traces data across module boundaries — types match, formats match, errors propagate. One checks data integrity — uniqueness constraints, cascade deletes, orphan prevention. One checks codebase consistency — if you handle auth one way in endpoint A, do you handle it the same way in endpoint B.

First run found 19 issues. The reviewer had caught 3 of them across all 8 rounds. The tool runs in 60 seconds. The reviewer takes 6 minutes.

The tool isn't smarter. It's just looking at the system instead of the diff. The reviewer sees what changed. The tool traces where the change goes.


The uncomfortable realization from this project isn't about code review. It's about when to do the hard work.

I built six phases of a CRM in a day. That felt productive. Then I spent 8 rounds fixing review comments. That felt like a slog.

But the slog was where the real engineering happened. The day of building was assembling components. The eight rounds of review were discovering how those components actually interact. The building was the easy part. The integration was the work.

If I'd traced the full data flows before pushing — before the first review, not after the sixth — most of those 8 rounds wouldn't have happened. Not because the code would have been perfect, but because I would have found the boundary issues myself, in context, where fixing one doesn't create another.

The lesson: don't push code that you haven't traced end-to-end. Not "it compiles" or "the tests pass." Traced. Pick up the user's action at the entry point and follow it through every file it touches until it produces output. Check every boundary crossing. Check every implicit contract. This takes 30 minutes for a feature that spans 10 files. That's less time than a single round of review.


I merged at 4 out of 5 after round 8. Two new rules for myself:

First, fixes are bugs. Every fix you push creates new review surface. Treat each fix with the same scrutiny as the original code. If you wouldn't push the original code without tracing it, don't push the fix without tracing it.

Second, trace before fixing. When a reviewer flags an issue, don't jump to the fix. Read the file. Read everything connected to it. Understand why the bug exists in the system, not just in the code. The reviewer describes problems correctly. They often miss downstream context.

The CRM works. Full-funnel attribution, event tracking, deal pipeline, team management, activity timeline. Replaced $3,600/month of HubSpot with a weekend of building and a week of learning that the building was the easy part.

That ratio — a day to build, eight rounds to integrate — is the honest cost of software. Anyone who tells you the building is the hard part hasn't shipped yet.

Comments

Loading comments...