Categories
Practices

Talking About Technical Debt: When to Fix It, When to Let It Ride

Every codebase past its first few months has technical debt, and every team has at least one engineer who wants to stop and fix it now, and one who wants to keep shipping features and deal with it later. Both are right some of the time. The actual skill isn’t picking a side – it’s telling the difference between debt that’s quietly costing you every sprint and debt that’s genuinely fine to leave alone.

The metaphor is more useful than the argument

Ward Cunningham coined “technical debt” to describe a real trade-off, not a moral failing: shipping the fast, imperfect version now is sometimes the right call, as long as you intend to pay it down before the interest – the ongoing cost of working around it – outweighs what you saved. Martin Fowler’s writeup on the concept is worth reading in full, particularly the point that debt taken on deliberately and debt accumulated by accident are very different situations that deserve different responses (martinfowler.com/bliki/TechnicalDebt.html).

Sort it into a quadrant before arguing about it

Fowler later extended the idea into a simple two-axis split: deliberate versus inadvertent, and reckless versus prudent. A deliberate, prudent shortcut – “we know this doesn’t handle multi-currency yet, we’ll add it when a customer needs it” – is a normal engineering decision. Reckless, inadvertent debt – code nobody understood was a problem until it broke – is the kind that deserves a real conversation about why it happened (martinfowler.com/bliki/TechnicalDebtQuadrant.html). Naming which quadrant you’re in defuses a surprising amount of the argument, because it separates “was this the wrong call” from “is this worth fixing now.”

Ask what it’s actually costing this month

Vague debt (“this module is a mess”) never wins a prioritization argument against a customer-facing feature with a deadline. Specific, recurring cost does. Track it the boring way: every time a piece of debt slows down an unrelated task – a bug takes three hours to fix because the module has no tests, a feature takes two extra days because the data model doesn’t support it cleanly – log it against that piece of debt.

# debt-log.md entry
Area: order pricing calculation
Cost this month: 2 incidents, ~9 engineer-hours
Why: discount logic is duplicated in 3 places, changes require
     updating all three or a discount silently doesn't apply
Fix estimate: ~2 days to consolidate into one function
Decision: schedule for next sprint - cost is now exceeding fix estimate

Once the cost is visible in hours instead of vibes, “when to fix it” becomes an ordinary prioritization decision instead of a philosophical one.

Explaining it to people who don’t read code

A product manager or founder doesn’t need the implementation detail, but they do need the business consequence, and it’s on engineers to translate one into the other. “The discount logic is duplicated in three places” means nothing to someone prioritizing a roadmap. “We’ve had two pricing bugs reach customers this month because of how this is built, and the next feature in this area will take twice as long” is a sentence anyone can weigh against other priorities. Keep the debt log from the earlier example somewhere non-engineers can see it, not buried in an engineering wiki – it turns “just trust us” into a conversation with actual numbers.

Piggyback on feature work when you can

The easiest debt to pay down is the debt sitting directly in the path of a feature you already need to build. If you’re touching the pricing module for a new discount type anyway, that’s the moment to consolidate the duplicated logic, not six months later in a dedicated cleanup ticket that will keep losing to higher-priority work. This isn’t a substitute for the dedicated time mentioned below – some debt sits in code nobody’s touching for other reasons – but it’s the cheapest debt you’ll ever pay off, so take the opportunity when it appears.

Debt that’s fine to leave alone

Not all debt needs a plan. Code that’s ugly but stable, isolated, rarely touched, and not on a path anyone’s about to build on top of – leave it. The Agile Alliance’s definition of technical debt makes this point well: the debt itself isn’t the problem, the compounding interest is, and code nobody touches doesn’t compound (agilealliance.org/glossary/technical-debt). Refactoring code just because it offends you, with no plan to build on it soon, is time you could have spent on something that pays back.

Make room for it explicitly

The most durable fix isn’t a big cleanup sprint – it’s a standing habit. Reserve a fixed slice of every sprint, even 10-15%, for debt work chosen from the log above, ranked by cost. This keeps debt paydown from competing head-to-head against features in every single planning meeting, which is a fight debt usually loses until it’s already expensive.

The goal was never a debt-free codebase – that doesn’t exist on a real product with real deadlines. The goal is debt you chose on purpose, that you can see the cost of, and that you’re paying down faster than you’re taking it on.

Categories
Practices

Code Review Without Friction in a Small Team

On a five-person team, code review can go one of two ways. Either it’s a rubber stamp – “LGTM” thirty seconds after the PR opens – or it turns into a slow-motion argument about tabs versus spaces while the actual feature sits unmerged for three days. I’ve lived through both, and neither is what review is for. The goal is narrow: catch bugs before production, keep the codebase coherent, and spread knowledge of the system across more than one head. Everything else is optional.

Decide what review is actually checking

Before you can fix review friction, agree on what a reviewer is responsible for. In practice that’s usually three things: does this change do what it claims to do, will it break something else that isn’t obvious from the diff, and can the next person who touches this file understand it without asking you. Style, naming preferences, and “I would have done this differently” are not on that list. If your team keeps arguing about formatting, that’s a linter and formatter problem, not a review problem – configure Prettier or Black and stop discussing it in PRs.

Put a number on response time

The single biggest source of review friction on small teams isn’t disagreement, it’s latency. A PR that sits for two days loses context – the author has moved on to something else, and picking it back up costs more than the original review would have. Agree on a norm: reviews get a first pass within a few working hours, not “when I get to it.” Google’s engineering practices guide makes this point directly – a reviewer should respond quickly even if the response is “I don’t have time for a full review today, but here’s a quick pass” (google.github.io/eng-practices – review speed). On a team of five, this is a habit, not a policy document – it just needs one person to model it consistently.

Label feedback by severity

Most review friction comes from ambiguity about whether a comment is a blocker. “Consider renaming this” reads very differently depending on whether the author is expected to act on it before merging. Prefixing comments removes the guesswork:

blocking: this will throw if `user` is null - handle it before merge
nit: could shorten this to a ternary, up to you
question: why do we retry here but not in the sibling function?
praise: nice catch on the race condition in the original PR

This is close to what the Conventional Comments spec formalizes (conventionalcomments.org). You don’t need the full spec – just the habit of marking non-blocking suggestions as non-blocking so the author can merge without a second round-trip.

Keep PRs small on purpose

A 40-line PR gets reviewed in ten minutes. A 900-line PR gets an “LGTM” without being read, because nobody has an uninterrupted hour to actually follow it. If a feature is large, land it in a sequence of small, independently reviewable PRs – behind a feature flag if it isn’t ready to ship. This is more work for the author up front, splitting a change into logical steps, but it’s the single highest-leverage habit for making review fast and actually useful rather than theatrical.

Review your own diff before anyone else does

The fastest review is the one that never needs a second round. Before requesting review, open your own PR as if you were the reviewer – read the diff top to bottom, not your editor’s view of the whole file. This catches the leftover debug print, the unrelated formatting change that snuck in, the TODO you meant to resolve before pushing. On a small team, a self-reviewed PR routinely needs one comment instead of five, which is the difference between a same-day merge and a PR that bounces back and forth for two days.

Assign, don’t broadcast

“PR ready for review” posted to a channel with no assignee diffuses responsibility – everyone assumes someone else will pick it up. On a team of five, just assign a specific reviewer, and rotate who reviews what so knowledge doesn’t pool in one person. If someone owns a part of the codebase, a lightweight CODEOWNERS file will auto-request them without you having to remember.

Know when to skip it

Not everything needs full review. A one-line typo fix, a config value bump, a revert of a change that broke the build – these can go through with a rubber stamp or even bypass review if your team trusts the author and the change is trivially reversible. Reserve careful review for what actually carries risk: anything touching auth, billing logic, data migrations, or public API contracts. Treating every change with the same ceremony is how review turns into overhead instead of a safety net.

None of this requires new tooling or a review policy doc nobody reads. It requires the team agreeing, once, on what review is for and what “blocking” means – and then a couple of people modeling fast, specific, non-personal feedback until it’s just how the team works.