Over the past few years, I’ve been through several projects - each full of talented, passionate developers.
But often… I was the only one asking the uncomfortable questions:
Why aren’t we using TypeScript?
Why are we still running Node 12 - which should already rest in peace?
Why are we keeping this one ancient library that blocks every upgrade?
Why don’t we bump the framework version?
And you know what?
Most developers didn’t argue. They knew. They were painfully aware of the problem.
But they couldn’t do anything - because “business doesn’t have time or budget for refactor.”
Except… sometimes those “good enough” technologies are not good enough at all.
They’re ticking time bombs, quietly waiting to go off.
As I mentioned earlier, my talk at js-poland.pl is coming up soon, and I’ll be speaking about migrating legacy code to modern frameworks.
And… I might’ve gotten a bit too obsessed with the topic lately. 😅
My previous post, “jQuery will outlive half of today’s JavaScript frameworks - here’s why”, sparked so many thoughtful comments and DMs that I felt I had to continue the conversation.
So here’s the next part - about the human side of migration.
The Real Problem: Communication, Not Competence
Product owners often don’t understand the true scale of the problem - not because they’re ignorant, but because we don’t speak their language.
When a dev says “we have tech debt” or “this is a security risk,” a non-technical PO hears: “They want to rewrite things and delay my roadmap.”
Meanwhile, you, the engineer, are thinking:
“If we don’t touch this soon, it’s going to blow up in production - and it’ll be on us.”
And that’s the truth: you are responsible for your code.
If a security patch stops arriving, or the product gets hacked, or the client walks away because of outdated tech - it’s not the PO who’ll get blamed. It’s you.
How to Make Product Owners Actually Listen
“It's outdated” doesn’t land.
You need to show the business impact, not just the tech reason.
Here’s a small cheat sheet for translating dev talk into business talk:
| Technical problem | Business impact | Measurable signal |
|---|---|---|
| No TypeScript | +15–30% slower code reviews, more regressions | PR cycle time, bug count |
| Node EOL | Security risk, potential client audit failure | SAST/DAST scan results |
| Old framework | No vendor support, growing migration cost | Slower velocity, merge time |
You don’t need fancy dashboards - use what you already have in your CI/CD or analytics tools.
Trends are more powerful than numbers anyway.
The 1-Minute Pitch to Your PO
Problem: “We’re running Node 12, which no longer receives security patches. That increases audit and compliance risk.”
Impact: “We’re spending roughly 12h/month maintaining compatibility and losing potential enterprise clients who require supported runtimes.”
Plan: “We can fix this over 3 sprints - half a day per sprint. No feature freeze, no rewrite.”
Success metric: “Green security scan, reduced bug count, and faster onboarding.”
Mitigation: “If a library breaks, we’ll use a local shim and toggle rollback.”
This structure works because it speaks in outcomes and risks, not code.
It turns a refactor from ‘tech work’ into ‘risk management’ - which is exactly what it is.
Small Wins: The Incremental Path
Modern migrations don’t need big-bang rewrites.
You can upgrade incrementally, with a rollback plan at every step.
TypeScript in a JS project
- Start with
allowJsandcheckJs. - Add JSDoc types in critical files.
- Gradually raise
strictsettings each sprint. - Document patterns for newcomers - onboarding eats most of your ROI.
Framework upgrades
- Use the Strangler Pattern - new features go into “the new world,” while old ones stay stable.
- Use feature toggles and canary deploys.
- Add contract tests to ensure both worlds behave the same.
Node / runtime updates
- Test the new version in CI first (matrix build).
- Swap out blocking libs or shim them temporarily.
- Deploy to 5% of traffic before the full switch.
A Tiny ROI Example (use this in your slide deck)
- Current cost: ~12h/month fixing regressions due to missing types.
- Investment: 24h adding TypeScript incrementally.
- Payback: 3 months. After that - pure savings.
You don’t need perfect data. Even rough numbers change the tone of the conversation.
Definition of Done for a Migration
✅ Unit + contract tests pass in CI
✅ Rollback plan tested in staging
✅ Documentation updated
✅ Performance + error metrics steady
✅ Team knows how to build on the new setup
Make “migration tasks” as small as feature tickets.
Refactors don’t need to be scary - just scoped and measurable.
Quick Sprint Checklist
- Enable
checkJsincore/price. - Add JSDoc or TS types for public API.
- Tighten ESLint or TS rules one step.
- Add contract tests for
/ordersendpoints. - Introduce feature toggle
uiV2.orders.enabled. - Canary release to 5% of users.
- Update README with “How to migrate this module.”
This approach builds trust - your PO sees progress without losing delivery speed.
“But what if I get fired for doing this?”
Honestly? You probably won’t.
In fact, you’ll look better when the first question comes from a client or auditor:
“Which version of Next.js are you on?”
“Do you still use Node 12?”
You’ll be the one who has the answer.
The one who manages risk instead of waiting for it.
What about you?
Have you ever tried to convince your PO to approve a refactor - or did you just do it quietly and hope no one noticed? 😅
I’d love to hear your stories (or scars) in the comments. 💜
Top comments (16)
I usually just refactor quietly, honestly. Most of the time I only touch the parts I’m confident about lol
the small things that eventually snowball into bigger structural improvements.
Where I work, most managers won’t approve refactoring unless something is literally breaking like the whole system going down. Even if features are getting slow, they still see it as “working,” so they don’t want changes.
It’s annoying, so I just do it silently. After years of ninja-migrating and cleaning things up, I’ve actually developed a pretty sharp refactoring instinct haha
Oh, I feel this so much. Quiet refactors are basically a whole underground dev tradition at this point 😂
Most improvements in legacy projects happen exactly because someone silently fixes “one tiny thing” that snowballs into real structure later.
And yeah - a lot of managers only react when the system is literally on fire, so everything else gets labeled as “still working.”
But honestly? That instinct you built - knowing what you can safely clean up and where to push - that’s real engineering maturity.
Your future teammates (and future you!) are 100% benefiting from those ninja upgrades, even if no one notices 😅
There is this component we have, that makes heavy use of the deprecated
KeyboardEvent.keyCodeproperty.MDN strongly suggests to switch to i.e. the
keyproperty.I just can't convince my PO that we need to update. But I also am not really sure if we need to.
Honestly, keyCode is one of those funny “deprecated but still kinda works everywhere” APIs — so I totally get why you’re unsure.
The way I usually frame it is:
If it’s isolated to one component and it’s not blocking any browser or accessibility requirements, it’s not an urgent migration.
But switching to event.key does future-proof things and avoids weird inconsistencies (especially on international keyboards).
So it’s one of those “won’t save the world today, but will save you from a headache later” changes.
If your PO won’t prioritize it, you can still refactor it gradually whenever you’re touching that component anyway — tiny, low-risk steps.
That’s often the easiest way to get rid of deprecated APIs without fighting for a full sprint ticket 😄
Really well said. Most teams don’t push back because they lack skills — it’s because they don’t know how to translate technical risk into business impact. Your examples made that gap very clear. I liked the “1-minute pitch” framework… simple, measurable, and something any dev can use in their next sprint meeting. Refactoring isn’t about rewriting — it’s about reducing risk before it becomes a fire. Great perspective.
Thank you - that really means a lot.
And you’re absolutely right: most teams don’t struggle with the technical side of refactoring, they struggle with the storytelling around it.
Once you frame a refactor as risk reduction - instead of “extra work” or “a rewrite” - suddenly the conversation becomes way more productive.
I’m glad the 1-minute pitch resonated!
It’s honestly one of the simplest ways to help devs advocate for themselves without turning every meeting into a 30-minute architecture debate 😅
Here’s to more teams treating refactoring as part of delivering value - not as a nice-to-have. 💜🚀
The nice thing about most refactorings is that they can be done "silently" and step by step.
I remember one of the biggest blocking refactorings was the switch from concatenating all JS Files to using ES-Modules. All files had to be touched. Wet tried to have as few branches as possible, because merging was painful.
And then we saw all the circular dependencies... 🤮
The nice thing about our codebase is, that it has no framework dependencies and therefore no painful upgrades that affect every module.
But hey, it's painful enough to update Jest.
I still had no time to upgrade ESLint.
Oh wow, the ES-modules transition is exactly the kind of refactor that looks small on paper and then detonates half the repo 😅
Touching every file + avoiding branches + circular deps… that’s the legacy trifecta right there.
And honestly, props to you - doing that without any framework dependencies is both a blessing and a curse.
Sure, no painful Angular/React mega-upgrades… but then Jest or ESLint updates show up like mini-boss fights anyway 😂
Silent, step-by-step refactoring really is the only sane way through this stuff.
One day you blink and realize the codebase is actually… healthy.
(And hey, good luck with that ESLint upgrade - may the gods of indentation be with you ✨)
Really strong piece. I’ve been in too many teams where “no time to refactor” is the default excuse, and in my experience building AI systems, that mindset is dangerous. If you don’t clean up as you go, technical debt accumulates like interest, and what “works for now” becomes fragile later.
When we’re orchestrating agents, messy code isn’t just an annoyance, it breaks the reliability of the entire system. I’ve learned that the hard part isn’t writing new features, it’s maintaining clarity of intent: making sure the code is understandable, testable, and safe to change.
If developers don’t insist on refactoring, they’re not just avoiding work, they’re choosing long‑term risk over short‑term speed. And ultimately, that gamble rarely pays off.
On my team, our ensembles include refactoring in their tasking of the story. The tasks evolve along with the work. As developers, we're constantly experimenting with every change we make, especially when we're working on complex problems. If we don't take time to refactor, we're not enabling options, we're restricting them.
I love this so much.
Refactoring as part of the story instead of a separate, scary “tech debt ticket” is exactly how healthy teams work.
You’re totally right - tasks evolve as we build, and pretending code stays static while features grow is just… fiction.
If we don’t refactor along the way, we’re basically choosing fewer future options just to move slightly faster today.
It’s so refreshing to hear a team treat refactoring as experimentation and learning, not as a distraction.
That mindset is what keeps a codebase flexible instead of fragile.
Honestly, this is the kind of engineering culture I wish more teams would normalize. 💜
Small steps in improving code can really make a big difference and make the project stronger. 💻🚀
Absolutely - those tiny improvements add up faster than people think.
Most “big refactors” are really just years of small, thoughtful steps hiding behind the scenes.
A stronger project is almost always the result of consistent, quiet care. 💜🚀
Great write up @sylwia-lask , recommended read!
Good
Some comments may only be visible to logged-in visitors. Sign in to view all comments.