Article
Problem #2/100: Why one bugfix breaks three other things
Fix the table, break the chart and the header. The vertical tangle inside one method that does five things — each silently dependent on the others — and how to defuse it without a rewrite.
Your developer fixes a bug in a report. It's a small fix — the columns in a table were slightly misaligned. The fix ships. The table looks perfect.
Two days later, two new tickets come in. The chart at the bottom of the report is overlapping the table. The logo in the header shifted down. Neither ticket mentions the table. Neither makes sense to the developer who fixed the original bug. But they're all connected — invisibly.
This is Problem #2 in a series of 100 I'm documenting from real SaaS projects. If Problem #1 was about infrastructure responsibilities bleeding into business logic (horizontal tangle — one concern smeared across many files), this one is about the vertical tangle: one function that does five things, each quietly dependent on the others.
What you're seeing from your seat
You don't need to read code to recognize this. Here's what it feels like:
"We fixed it, but now something else is broken." This is the hallmark. Not a rare event — a pattern. Fixes that create new bugs. You start wondering whether your team is testing at all. They are. The code is just structured so that changes in one place ripple into places nobody expected.
Small changes get big estimates. The product owner asks for a simple tweak — move the logo, add a column, change a font size. The developer says two days. Not because the tweak is complex, but because they need to verify that nothing else shifted when they touched that part of the code.
Developers avoid certain files. There's always one file in the project that everyone dreads opening. It works, but nobody fully understands why, and nobody wants to be the person who breaks it. That file is almost always a long method that does many things in sequence.
You can't add features without regression testing everything. New features should be additive — you're adding something, not changing everything. But when the foundation is a single tightly-wound function, adding anything means risking everything.
The assembly line analogy
Imagine a factory assembly line where every station is actually one person doing all the work.
Station 1 doesn't just attach the wheels. The same person attaches the wheels, paints the door, wires the dashboard, and installs the windshield. They do it in order, top to bottom, and each step uses the same set of tools sitting on the same bench.
It works. Cars come out. But one day the windshield spec changes — slightly thicker glass. The worker adjusts their grip. This shifts their elbow position, which bumps the paint can, which drips onto the dashboard wiring. Three defects from one change.
A well-run factory separates stations. The wheel person only does wheels. The paint person only does paint. Each station takes the car in one state and passes it to the next. If the windshield spec changes, only the windshield station changes. The paint station doesn't know, doesn't care, doesn't break.
In your codebase, a spaghetti method is the one-person station. It looks efficient — everything in one place, easy to read top to bottom. But every change has an unpredictable blast radius.
What this looks like in code (simplified)
Here's a method that generates a PDF report — the kind of feature every SaaS builds eventually. This is the "clean" version, after someone organized it with comments and clear blocks:
function generateReport(data: ReportData): void {
const doc = new PDFDocument({ layout: "landscape", size: "A4" });
// Header section
if (data.header) {
doc.fontSize(18).text(data.header.title, 50, 40);
if (data.header.logo) {
doc.image(data.header.logo.src, 400, 30, { width: 100 });
}
}
let yOffset = data.header ? 80 : 40;
// Table section
if (data.rows) {
for (const row of data.rows) {
// ... render each row, update yOffset
yOffset += 20;
}
}
// Charts section
if (data.charts) {
yOffset += 30;
for (const chart of data.charts) {
doc.image(renderChart(chart), 50, yOffset, { width: 500 });
yOffset += chart.height + 20;
}
}
doc.save("report.pdf");
}
See that yOffset variable? It starts at the top of the page and threads through every section — header, table, charts. Each section reads it and changes it. That's the shared workbench in the analogy.
Fix a bug in the table (column widths were wrong) → the table now takes more vertical space → yOffset shifts → charts render in the wrong position → two new tickets.
After separating each section into its own function:
function generateReport(data: ReportData): void {
const doc = new PDFDocument({ layout: "landscape", size: "A4" });
let y = 40;
if (data.header) y = renderHeader(doc, data.header, y);
if (data.rows) y = renderTable(doc, data.rows, y);
if (data.charts) y = renderCharts(doc, data.charts, y);
doc.save("report.pdf");
}
Each function takes a position, does its job, and returns the next position. Fix a bug in renderTable? It can't affect renderHeader — they don't share variables. Add a new section (footer, watermark)? Write one function, add one line. The blast radius of any change is one function, not the entire report.
The business impact
Regression cost compounds. Every release that breaks something unrelated costs you twice: once to fix the new bug, and once in the trust your team loses in the codebase. After enough regressions, developers stop making small, fast changes and start making big, slow, "safe" ones — which are actually more dangerous because they're harder to review and test.
Estimate inflation. When a developer says "two days for a font change," they're not padding the estimate. They're accounting for the time they'll spend verifying that nothing else broke. In a codebase full of spaghetti methods, that verification cost is added to every task. Over a quarter, estimate inflation alone can cost you 30–40% of your development velocity.
Parallelization is blocked. Two developers can't work on the same method at the same time without merge conflicts. If your report logic is one method, only one person can touch reports at a time. Separate it into functions, and one developer adds charts while another fixes the table — simultaneously, no conflicts.
Onboarding stalls. A new developer can understand renderTable in ten minutes. They can understand a 120-line generateReport method in maybe two hours — and they'll still miss a hidden dependency between sections. Every spaghetti method is onboarding friction that you pay for with every hire.
What to do — without a rewrite
1. Identify the blast radius. Find the method. Look for let variables declared at the top that are read and mutated further down. Each one is a hidden wire connecting sections that look independent. The more wires, the bigger the blast radius.
2. Extract from the bottom up. Start with the last section — the one closest to the end of the method. Extract it into its own function. That section has the fewest things depending on it, so the risk of breaking something is lowest. Test. Then extract the next one up. Work backward.
3. Make the contract explicit. Each extracted function should take what it needs as arguments and return what the next function needs. No shared mutable variables. The method that remains should read like a table of contents — "do this, then this, then this" — with no logic of its own.
4. Add tests per section, not per report. Once sections are extracted, you can test each one independently. renderTable gets its own test with mock data. You'll catch bugs before they ripple into other sections. This is the real payoff — not just cleaner code, but faster and more confident debugging.
What's the method in your codebase that everyone avoids touching? Drop it in the comments — I pick Problem #3 from what founders and developers actually deal with.
If your team's estimates keep growing and every fix creates a new ticket, that's exactly the kind of tangle I help SaaS founders sort out at mvpforstartup.com.