Last week priced what “just ship it” actually costs, in three numbers a manager already tracks: integration lead time, time to productive, time to resolve. That post was aimed outward, at whoever approves the roadmap, because that’s who signs off on the work. This one is aimed at a person we meet much earlier, and who says no long before anything reaches the roadmap.
Let’s say we work on IoT devices and we want to clean up the firmware rollout state machine. Five states (pending, downloading, staged, committed, rolled-back), and a switch statement that’s grown a branch every release, then quietly copied itself into half a dozen services. We bring it to our team lead, Jakub. “Not now”1, he says. “We can’t afford the risk.”
The easy read is that Jakub doesn’t care about quality. He’s a manager now, he’s forgotten what good code feels like, he just wants the tasks closed. That read is comfortable, and it’s exactly why the conversation keeps losing. Jakub isn’t against the work. He’s measured on something the work briefly makes worse, and until we argue in the terms his job is scored in, his answer will keep being no. This post is about what those terms actually are, and how to speak them.
Our Lead Isn’t Wrong, They’re Optimizing Something Else
Start by giving Jakub the credit he’s earned. Odds are he was a strong engineer not long ago, and he can still read a switch statement. He knows the rollout state machine is bad. He has probably cursed it himself, at least once, in a merge he’d rather forget. The reason he says no isn’t that he can’t see the problem. It’s that he’s standing on a different scoreboard than we are.
When a firmware rollout half-bricks a fleet of devices in the field, the person who writes the summary and explains it upward is Jakub, not us. When the sprint slips, it’s his name on the slip. That accountability is the whole story. He is not being lazy or short-sighted. He is carefully optimizing for the things he is actually held to, and none of them is “the code is nicer to work in.”
This is the reframe the rest of the post runs on. “They don’t care about quality” and “they’re optimizing something other than quality” produce the exact same no at the moment we ask. But only one of them tells us what to say next. The first is a dead end: if he doesn’t care, there’s nothing to do but resent him. The second is a map. It says his “no” has reasons, the reasons are specific, and specific reasons can be answered.
There are three of them, and they’re worth naming one at a time, because the answer to each is different.
Measured on Delivery, Not Code Health
Jakub’s quarter is counted in features shipped and dates hit. That’s the number his own boss asks about, so that’s the number he protects. Refactoring the rollout state machine costs him a visible dip now and pays him back invisibly later, which is the worst possible trade when we’re the ones being measured.
Look at how the two halves of that trade actually show up. The refactor sprint shows up on the burndown as a week where nothing shipped, with Jakub’s name on it2. The five features that go faster afterward show up as five features that went fine. Nobody logs “this rollout channel took three days instead of eight because we cleaned up the state machine last quarter”. The cost is attributable. The savings are anonymous.
A lead who is rational about his own scoreboard says no to that trade every time, and he isn’t wrong to. This is the part worth spending more time with: the no is correct, given his incentives. If we were measured the way he is, we would make the same call. The fix, then, is to change the numbers on the board, so the trade stops being a loss on the metric he answers for.
They Own the Deployment Risk
“We can’t afford the risk” isn’t a dodge. It’s the most honest thing Jakub says all meeting. The rollout state machine works. It’s ugly, it’s fragile, but right now it ships firmware to real devices in the field without bricking them. Refactoring it means reaching into code that currently works and hoping it still works when we finish.
Here’s what “reach into” actually means, and why it’s not a small ask:
// As it ships today: one switch on the rollout state, copied into every service that reacts to it
switch (rollout.state()) {
case PENDING: scheduleDownload(rollout); break;
case DOWNLOADING: checkProgress(rollout); break;
case STAGED: awaitCommitWindow(rollout); break;
case COMMITTED: markLive(rollout); break;
case ROLLED_BACK: restorePrevious(rollout); break;
// new state? add a branch here, and in the five other switches that were supposed to stay in sync
}
// The target: each state owns its own transitions; a new state is a new class, and nothing else moves
interface RolloutState {
RolloutState onDownloaded(Rollout rollout);
RolloutState onCommitted(Rollout rollout);
RolloutState onFailed(Rollout rollout);
}That switch is in many files: it’s copied, with small variations, into the download service, the telemetry pipeline, the audit log, and wherever else somebody once needed to know what a rollout was doing. Getting from the top of that block to the bottom means finding every copy and routing it through the new type. Miss one path and a device downloads firmware but never commits, or commits but never gets marked live. In the field. On hardware someone has to drive to.
Now put ourselves in Jakub’s shoes. If that refactor goes wrong, he’s the one in the incident review, and “it’s cleaner now” is not a sentence that survives that room3. “It works” is defensible upward. “It’s cleaner” is not.
The real content behind “we can’t afford the risk” is that the risk is personal, and it’s uninsured. He carries it alone, and there’s no upside on his scoreboard big enough to make carrying it feel worth it. Until we change that, his caution is just good self-preservation.
Nobody Gave Them an ROI Framework
Hand Jakub a feature and he can tell us what it’s worth. It has a revenue case, or a deadline, or a customer with a name attached. He can rank it, defer it, trade it against three other features, because it comes pre-loaded with a unit he can do math in. Hand him “we should refactor the rollout state machine” and he’s got nothing to weigh it with.
It isn’t ignorance or indifference. Nobody ever handed team leads a way to price internal quality work. Everything else that lands on his desk arrives with a number: story points4, revenue, a due date, a support-ticket count.
Refactoring shows up in adjectives. Cleaner. Safer. Nicer to work in. Adjectives lose to numbers every time, and they should. A lead who greenlit work on the strength of “it’s nicer” wouldn’t last a quarter.
So the request that reaches him is structurally unfair to itself. It’s asking to be prioritized against numbered work while showing up with no number. He’s not rejecting the work. He’s rejecting a thing he can’t evaluate, which is the responsible move when he’s the one accountable for the evaluation.
The good news buried in that is the turn the rest of the post walks through: the missing number is the one root cause we can remove outright. We can’t rewrite his scoreboard or lift the deployment risk off his desk, but a number makes both easier to carry, and that number is ours to hand him.
Give the Refactor a Number
Last week’s whole post5 was an argument for exactly this move: stop claiming the code is messy, and put a business number on it. It laid out what to measure across the team (integration lead time, time to productive, time to resolve), and why the trend line matters more than any single reading. I won’t re-run that here. What changes this week is the audience.
Last week we were building a case for the roadmap, a team-level argument for whoever sets priorities. This week we’re standing in front of one person who has already said no, and that’s a different job. We don’t need three metrics and a trend line to move one person. We need one number, in the currency he actually named.
Jakub said risk, so we should give him the risk math for the state machine we both know. That switch lives in six services. Today, adding a rollout state means editing six files and praying the tests catch the one we missed. After the refactor, it’s one new class implementing one interface. Then count it out loud, in front of him:
“The last channel took four days, and two of them were just hunting down call sites. We’ve done three of those this quarter. The refactor is five days once, and every channel after it is an afternoon.”
That’s not an aesthetic argument anymore. That’s delivery risk and integration time, in his language, about his scoreboard. It converts “the code should be better” into “here’s how we ship the next three rollouts faster and with fewer places to get it wrong.” One honest number he can check beats a handful he has to take on faith. Pick the concern he voiced, put a number next to it, and let the number do the reasoning.
Answering Each of His Concerns
The number gets us a hearing, but it doesn’t close the deal. Jakub still has all three worries. Each one has a real answer, not just a brush-off. Start with the one he named out loud.
The deployment risk: make it reversible, not one big bet. His fear is a single scary change that touches six services and breaks firmware in the field. So instead of handing him that change, start with characterization tests that pin down what the state machine does today, bug for bug, before we touch how it does it6. Then strangle it: route one service through the new RolloutState type, leave the other five on the old switch, ship it, watch it for a week. The refactor stops being a single Friday-afternoon rewrite and becomes ten boring, small changes, each one reversible on its own. That’s the answer to the risk he owns: no single step is big enough to write an incident review about.
The velocity dip: fold it into the next feature. His fear here is the week with nothing shipped and his name on it. So don’t ask for that week. Scope the refactor tightly, time-box it, and then bolt it onto the next feature that has to open this code anyway. There’s always another rollout channel coming. Do the cleanup as the first half of shipping that channel, and the cost stops being a standalone tax and becomes “this feature took a little longer, for a reason I can defend”. Shared cost, not a separate line item he has to justify.
Explaining it upward: arm him, don’t overrule him. His last fear is the one we can’t see, the conversation he has with his own boss. So give him the sentence for it. Hand him the ROI comparison as a thing he can forward, in his own words, with his own name on it7. We’re giving him a number he can defend upward, and leaving the decision where it belongs, with him. The version of this that actually works ends with Jakub owning the decision, not with us being right.
Reversible slices answer the risk he owns. Bolting the cleanup onto a feature already in flight covers the velocity dip on his scoreboard. And the forwardable number is the pricing framework he never had, which is what makes explaining it upward winnable. Every loop the post opened, closed on his terms rather than ours.
The Takeaway
Our lead’s “no” is almost never indifference. It’s a scoreboard we can’t see: he’s measured on delivery, he personally owns the deployment risk, and nobody ever gave him a way to price the work. We don’t win this by caring more about quality than he does. We win it by making quality legible on his scoreboard, in delivery risk and integration time, one number at a time.
Forward This to Your Team
If our refactoring keeps dying at “not now, we can’t afford the risk”, this is the post to forward, and the person to forward it to is the one who keeps saying it. Not as an accusation. The whole argument here is that the no is rational, that it comes from incentives he didn’t choose and mostly can’t see. Naming those incentives out loud, without blame, is what gives the no room to move.
Here’s the version for that person:
This article explains why engineering leads resist refactoring, and it’s not because they don’t care about quality. The author identifies three root causes: leads are measured on delivery velocity (not code health), refactoring carries deployment risk they’re accountable for, and they don’t have a framework for evaluating refactoring ROI. The article provides a simple ROI model: compare the time saved on the next 3-5 feature implementations against the refactoring investment. When the math is visible, the conversation changes. Relevant for any team where quality improvements keep getting deprioritized.
We should empathize with him instead of saying he doesn’t care enough. He does care. In fact, he cares about more things than we do. So instead of finger-pointing, we should hand him the scoreboard math he was never given and let him make the call with it.
Media attributions:
Cover image by the author (generated with Gemini)
"Not now" is the most optimistic phrase in engineering. It usually means "not ever", but it leaves the door open so nobody has to say it out loud.
The burndown chart is very good at showing what didn't ship this week and completely blind to why next quarter went faster. It is, in that sense, a perfect instrument for punishing refactoring.
The incident review is where "it works" and "it's cleaner" get sorted into "shipped" and "a judgment call". Jakub has been in that room before. He remembers which sentence saved him.
Story pointing (and, in general, estimation) is another lie that deserves its own post. Maybe one day when I can finally manage the PTSD it caused.
The full version, including why a single measurement is an anecdote and only a trend is a business case, is in last week’s post, The Cost of “Just Ship It”. This section assumes we’ve made peace with the idea and just need to aim it.
Characterization tests pin down what the code does now, not what it should do. We're not asserting it's correct. We're catching the moment we accidentally change its behavior, which is exactly the failure Jakub is afraid of.
The sign that we've done this right: he forwards our comparison to his boss as his own. That isn't credit theft. That's the entire goal.

