Framework migrations are a lot like moving houses. You think you're just packing boxes, but suddenly you're replacing plumbing, repainting rooms, and arguing with contractors. Except your codebase doesn't have a moving truck—it has hundreds of developers, legacy dependencies, and a deadline that gets pushed every quarter.
I've seen teams spend eighteen months on a migration that never shipped. I've also seen a group migrate a million-line app in six months by avoiding the usual traps. The difference? They knew which anti-patterns to sidestep. This field guide covers the core ideas—what works, what fails, and when you should just say no.
Where These Anti-Patterns Show Up in Real Work
The startup that outgrew jQuery and tried to rewrite in React overnight
I watched a twelve-person SaaS group do this last year. They had two weeks between funding rounds to ship a new dashboard — and someone declared jQuery a "technical debt emergency." The CTO closed the old codebase, spun up a fresh React repo, and asked five developers to rebuild fifty-seven screens from scratch. No gradual extraction, no shared state bridge, just a hard cutover scheduled for the end of the sprint. That sounds fine until you realize they had no wireframes, no component library, and three people who had never touched JSX. What broke first? Authentication. They spent four days re-implementing login flows that already worked — and the old jQuery version still handled billing in production. The migration was abandoned after week three. The startup is now running two apps in parallel, neither fully functional, and the dev group has dropped from twelve to seven.
The pattern here is almost comically common: a crew conflates "old tool" with "bad architecture." jQuery wasn't the problem — the spaghetti controller logic was. But rewriting in React without refactoring the underlying design just re-plated the same mess in JSX. — Senior Engineer, post-mortem retrospective
The enterprise that ran AngularJS and Angular 2 side by side for two years
A Fortune 500 insurance platform decided to migrate from AngularJS to Angular 2 in 2017. Their plan? Run both frameworks simultaneously, sharing a single DOM using ngUpgrade. That approach worked for the first six months — until the crew realized every new feature required writing hybrid components that imported directives from both frameworks. The bundle size ballooned to 4.2 MB. Build times hit twelve minutes. And the real killer? They had to maintain two routing systems, two change detection strategies, and two sets of dependency injections for every screen. The migration never finished. Two years in, the group quietly froze the effort and started wrapping AngularJS controllers in iframes instead. Not a rewrite — an admission. The anti-pattern here is assuming "incremental" means "painless." Incremental migration only works when you carve strict boundaries (a micro-frontend, a dedicated route) and kill the old code path with prejudice. Running two frameworks in the same page is not migration; it's deferred bankruptcy.
Most teams skip this: they never define what "done" looks like. Is done zero AngularJS files? Or is done all new features in Angular 2, while old screens stay frozen? Without a kill-switch date, the seam between frameworks becomes a permanent drag. — Platform Architect, enterprise migration postmortem
The crew that migrated to Vue and ended up rebuilding the same components twice
A mid-size analytics firm decided Vue would solve their build complexity. They had forty React components, a messy Redux store, and a manager who read "Vue is easier to learn." So they rewrote. Not piece by piece — they paused all feature work for six weeks and rebuilt the entire frontend in Vue 2. The catch? Their API layer, routing logic, and state management patterns were so tightly coupled to React's lifecycle that the Vue version just mimicked React patterns with Vue syntax. They shipped on time, but the app felt wrong — re-renders were sluggish, computed properties fought with watchers, and the crew spent another eight weeks refactoring the refactoring. The original React codebase was still in the repo, untouched. One engineer told me, "We migrated syntax, not architecture." That's the anti-pattern: treating framework migration as a mechanical search-and-replace rather than a design opportunity. If your components look the same in Vue as they did in React, you paid the migration cost but collected zero benefits. Worse — you now own two codebases with identical bugs.
What usually breaks first is state management. Teams copy their Redux ducks pattern into Vuex without questioning whether Vue's reactivity model could simplify entire concepts. It doesn't. You just add indirection. Honest advice: before you write one Vue component, delete the old store and prototype a feature with local reactive state. If that feels impossible, your migration will hurt. It will hurt for months.
Foundations Most Teams Get Wrong
Confusing 'migration' with 'rewrite from scratch'
The most common mistake I see is a group announcing a framework migration and then quietly spending eighteen months rebuilding every single feature, exactly as it was, in the new stack. That's not a migration. That's a rewrite with a different name, and it carries the same failure rate—roughly a coin flip. The trap is seductive: you look at the legacy code, you hate it, you tell yourself the new framework deserves a fresh start. So you refactor things that never needed refactoring. You redesign the data layer because the old one felt ugly. You add features nobody asked for. Six months in, you're still on the old framework in production, your stakeholders are furious, and the new code hasn't shipped a line to real users. The fix is brutal but simple: migrate one page at a time. Ship it. Prove the new framework works under load. Then repeat. Anything else is a bet against your own deadline.
Assuming the new framework solves all your problems
Teams often act like a framework swap is a magical performance patch. "We'll just move to framework Y, and the slow queries will vanish." No, they won't. The slow queries were caused by bad indexing, not the JavaScript library. The memory leak was in your custom event handling, not the rendering engine. A new framework exposes those problems—it doesn't fix them. I watched a staff migrate a calendar widget to a reactive framework and still have the same race condition, because they'd ported the broken logic verbatim. That hurts.
The harder truth is that the new framework introduces its own pitfalls. It might have steeper bundle-splitting requirements. It might handle error boundaries differently. It might make your existing test infrastructure obsolete overnight. You're not upgrading; you're trading one set of constraints for another. The question isn't "Is framework Y better than framework X?" The question is "Do we actually understand the operational cost of switching, including the two months of downtime while the QA crew learns the new tooling?" Most teams skip this:
We chose React because it had more stars on GitHub, and then spent three weeks figuring out how to mock the new router for our Cypress suite. We never thought about testing until we had to.
— Senior engineer, post-mortem on a stalled Vue-to-React migration
Underestimating the cost of running two frameworks simultaneously
The catch that kills projects quietly is the seam period—the months, sometimes years, when both frameworks live in the same codebase, served to the same users. That's not a technical detail; it's an organizational tax. You now have two build pipelines, two logging conventions, two deployment strategies for hotfixes. Your junior devs get confused about which pattern to follow. Code reviews slow down because reviewers need context-switch time. And your CI/CD matrix doubles—every PR has to pass linting and tests for both ecosystems.
I've seen teams burn sixteen weeks building a shared auth module that worked across both frameworks, only to realize the legacy framework's bundler couldn't tree-shake the new library's dependencies. The result? A 400KB payload increase for users still on the old route. Nobody planned for that. The fix—if you must have a seam—is to make it as short as possible. Define a hard cutover date. Freeze new features in the legacy system. Don't let the seam become permanent, because permanent seam means permanent drag, and that drag will eventually kill morale, then velocity, then the whole migration.
Patterns That Actually Work (If You're Careful)
The Strangler Fig Pattern Done Right — Incremental Replacement With Clear Boundaries
The Strangler Fig sounds obvious: wrap the old system, route new features to the new framework, cut over piece by piece. Obvious is not the same as easy. I have watched teams pick one endpoint, rewrite it in the shiny new stack, then spend two months fighting routing, auth tokens, and session state across two frameworks running side by side. That's not strangling — that's creating a hybrid monster that inherits the worst of both worlds. The pattern only works if you define a hard seam between old and new: a reverse proxy, an API gateway, or at minimum a shared middleware that knows which requests go where. Without that seam, every page load becomes a detective case. You lose a day per bug — sometimes more.
The catch is that teams under deadline skip the seam. They embed a Vue component inside a legacy Rails view, call it a day, and pray. That works for exactly three weeks. Then someone needs to pass a deeply nested prop, the legacy global CSS bleeds into the new component, and the whole thing turns into a debugging sinkhole. One concrete fix we used on a mid-size SaaS product: a feature flag that routes entire route groups to a micro-service running the new framework. Old code never touches the new service. New code never reads old session cookies. The boundary is brutal but clean. That approach bought us six months of parallel development without a single rollback.
Reality check: name the frameworks owner or stop.
Another trap: teams strangle the UI but skip the data layer. You can't just replace the frontend — the API contracts, error handling, and caching strategies need to match or you're building an impedance mismatch. Strangle in layers, not in screens. Start with one read-only endpoint, validate latency and error rates, then expand. It's slower at first. It saves a sprint of firefighting later.
Feature Toggles and A/B Testing — Validate the New Framework in Production
Feature toggles let you flip a switch and expose 5% of users to the new framework while 95% stay on the old. Sounds safe. The hard lesson is that toggles themselves add complexity — dead code paths, stale flags, and the temptation to keep both codebases alive forever. What usually breaks first is state: user sessions, cart data, or anything persisted across page transitions. If the old framework stores a session cookie with key user_id and the new one expects userId, the toggle doesn't help — you just break a subset of users silently.
Most teams skip this: run the toggles with real monitoring, not just "looks okay in staging." Track page load time, error count, and business metrics like signup completion or checkout rate. We had a migration where the new framework rendered the homepage 30% faster, but the analytics group noticed a 4% drop in newsletter signups. Turned out the new form component had a missing `aria-label` — accessibility broken, no error logged, but real users bounced. The toggle let us revert that single component in three minutes instead of rolling back the whole deployment. That's the power of careful toggling: surgical reversibility.
The downside nobody advertises — toggle fatigue. After twelve flags, your deployment pipeline becomes a spreadsheet. One team I consulted had 47 flags, half of them undocumented, and nobody knew which code paths were actually live. Burn your flags. Every month, delete every toggle older than two sprints. If you're scared to delete it, you shouldn't have used it in the first place.
'A feature flag that lives longer than the migration is not a flag — it's a permanent second system.'
— whispered by a tech lead at a conference after his third coffee
Micro-Frontend Isolation — Practical or Overkill?
Micro-frontends carve your app into independent deployment units: the checkout page runs the new Svelte app, the profile page stays in Backbone. Done carefully, it lets teams migrate at their own pace without blocking each other. Done sloppily, you get five different routers fighting over URL history and a bundle size that makes users on 3G wait twelve seconds for a login page. The pain point is shared dependencies — if the old app uses jQuery and the new one imports Material UI, the combined footprint can double before you render a single pixel.
What worked for a team I advised: they agreed to share exactly one thing — a thin shell that handled authentication and routing. Everything else was isolated behind iframes initially, then moved to Web Components once the migration hit 60% completion. The iframes were ugly (yes, scrolling issues, yes, accessibility holes), but they eliminated state bleed completely. That isolation meant the old team could ignore the new code entirely — no regression testing, no shared npm packages, no arguments about bundler config. It cost performance for the first three months. It saved a year of cross-team coordination hell.
Honest warning — micro-frontends are not for small teams. If you have fewer than five engineers, the overhead of maintaining multiple build pipelines, deployment targets, and integration tests will eat your migration budget. In that case, pick the strangler fig with a hard seam or just rewrite the whole thing in one shot and accept the risk. Not every pattern fits every project.
Anti-Patterns That Force Teams to Revert
The Big Bang rewrite—why it almost always fails
I have watched teams disappear into a Big Bang rewrite for six months, then twelve, then eighteen. The original codebase gathers dust. Business stakeholders lose patience. And when the new system finally emerges? It breaks in production within hours—and the rollback button gets smashed. The pattern is seductive: 'Let's just rewrite everything properly this time.' That sounds noble until you realize you've built a brand-new system with zero battle testing. No incremental validation. No feedback loops. Just hope and a deploy button.
The real killer is context loss. When you rewrite from scratch, you don't port over the hard-won bug fixes, the edge-case handling, or the implicit knowledge of why something was done a certain way. You inherit the framework's shiny features but lose years of production learning. One team I worked with spent nine months rebuilding a billing engine. On launch day, the first customer invoice failed—because the old system had a silent catch for currency rounding that nobody remembered to document. They reverted within 48 hours. That hurts.
— Observed at a mid-stage SaaS company, 2023
The alternative? Strangler Fig patterns. Carve off one endpoint. Migrate it. Test it in production for two weeks. Then do the next one. Boring? Yes. But boring keeps the lights on.
Copying old architecture patterns into the new framework
Most teams make this mistake thinking they're being 'pragmatic.' You move from Django to FastAPI, but you still write monolithic views with fat models. Or you jump from Angular to React, yet you keep using two-way data binding and service singletons everywhere. What's the point of migrating then? You've paid the cost—the retraining, the downtime, the deployment pipeline overhaul—without collecting the benefits. The framework is new; the headaches are the same.
The catch is subtle. Teams often don't realize they're doing it because the structural habits are baked into their code reviews, their folder conventions, their mental model of 'how web apps work.' I have seen a team adopt Nuxt 3 but still manually inject dependencies through a global registry—exactly how their old Express app worked. They never used Nuxt's module system. They never touched auto-imports. They basically built a slower version of their old app. The rollback happened when the new framework's performance regression couldn't be debugged—because they'd bypassed every optimization the framework offered.
We fixed this by mandating a spike: two developers spent one week prototyping a single feature using only idiomatic patterns from the target framework. That prototype became the reference. No shortcuts. No 'we've always done it this way.' If you can't articulate what the new framework buys you over the old one—in concrete, measurable terms—don't start the migration.
Not involving the whole team in the migration plan
A migration planned by two architects in a silo is a migration destined for the graveyard. I have watched this unfold: the architects produce a 40-page document. The rest of the team gets a Slack announcement. Then the questions start—questions the architects never considered. 'What about our custom auth middleware?' 'The reporting module depends on a library that doesn't exist in the new ecosystem.' 'Who's going to rewrite the ETL pipeline?' Silence. The plan unravels in the first sprint.
Odd bit about frameworks: the dull step fails first.
The anti-pattern here isn't about bad intentions; it's about information asymmetry. The people who actually know the code—the seniors who fixed that one bizarre race condition at 2 AM, the juniors who maintain the legacy test suite—hold the crucial context. Exclude them, and your migration plan is a fantasy. One team I knew spent three months migrating a search service, only to discover that the legacy system had a custom scoring algorithm that the new Elasticsearch version didn't support. The rollback took four hours. The blame took months.
Don't just ask for input; assign ownership slices. Let each developer own the migration of one module they already maintain. They know the traps. They know the weird data shapes. They will surface the problems before they become reversion triggers. That's not democracy—that's survival.
Maintenance and Long-Term Costs Nobody Calculates
The hidden cost of technical debt during migration
Most teams budget for the migration itself—engineer time, testing cycles, maybe a staging environment. They don't budget for the debt that keeps compounding after the cutover. I have seen a project where the migration team declared victory, handed off the new system, and within three months the old system was still running because three edge cases nobody documented never got ported. That sounds like a planning failure, which it was. But the real killer was the maintenance tax: every deploy to the new codebase required a mirror change in the legacy system. Two PRs. Two reviews. Two deployment windows. That tax never appears on any Gantt chart.
The catch is subtle. Technical debt during a migration isn't just bad code—it's the gap between what you think you migrated and what actually works. One team I worked with kept a "temporary" proxy layer that translated API responses between old and new formats. They estimated two weeks to remove it. Eighteen months later, that proxy handled 40% of production traffic and had grown its own bugs, its own test suite, its own on-call rotation. The debt was invisible until the proxy's author left, and suddenly nobody understood the translation rules.
What usually breaks first is data integrity. When you run two systems in parallel, writes must land in both places or the drift becomes a canyon. I have seen teams spend three sprint cycles reconciling a single table because a migration script ran in the wrong order during a maintenance window. That's not a lift-and-shift cost—that's a long-term line item nobody put in the budget.
How 'temporary' shims become permanent liabilities
The shim pattern is seductive. "We'll just wrap the legacy endpoint until we rewrite the service." That works for exactly one sprint. Then another team builds a feature on top of the shim, and now removing it would break two callers. Three months later, the shim has its own configuration, its own monitoring, and the original migration ticket is still sitting in "Backlog" with a stale estimate of 2 story points.
Wrong order. The shim should have been a warning signal, not a solution. Every time I see a team add a compatibility layer, I ask one question: "What is the date you will delete this?" If the answer is anything other than a specific sprint number, you have a permanent liability. The worst part is that shims hide the real migration cost—they mask the incompatibility that should have forced a harder decision earlier. You end up maintaining the shim, the old system, and the new system. That's three codebases to keep alive.
Here is the trade-off that rarely gets discussed: a shim buys speed today but charges compound interest forever. The decision isn't "shim or no shim"—it's "shim now and pay later" versus "fix the interface now and move on." Most teams choose the first option because it feels reversible. It isn't. Not really.
The cognitive load of maintaining two codebases and two build pipelines
This is the cost that exhausts teams before they even notice it. A developer working on the new system needs to context-switch to the old system for bug fixes. The build pipeline for the old system uses Jenkins; the new one uses GitHub Actions. Deploying to the old system requires a VPN and a specific JDK version that nobody can install without a ticket. The new system deploys with a single merge to main. That friction adds up—not in dramatic failures, but in lost hours every single day.
I have watched a senior engineer spend an entire afternoon debugging a CI failure on the legacy pipeline, only to discover the build machine had a different locale setting than the new pipeline. That's not a technical problem—it's a cognitive tax. Every time a developer has to remember which pipeline to use, which toolchain to invoke, which environment variables to set, the migration's hidden cost grows. The worst part is that nobody tracks this. It doesn't show up in velocity charts. It shows up in burn-out, in skipped tests, in the quiet resignation of "I'll just fix it in the new system next quarter."
Most teams skip this calculation entirely. They assume the parallel run is temporary—a few months, maybe a quarter. But I have seen migrations where the parallel run stretched past eighteen months because the old system had compliance requirements the new system couldn't meet. The cognitive load didn't just grow linearly; it compounded as new team members joined and nobody had documented which system owned which behavior. The result? The migration didn't reverse—it just stalled, with everyone maintaining both worlds forever.
One rhetorical question worth sitting with: if your migration costs more to maintain than the original system cost to run, was the migration actually an improvement? That's the math most leaders refuse to do.
'We kept the old system alive 'just in case' for fourteen months. When we finally turned it off, nobody noticed—except the three people who had been updating both codebases every sprint.'
— Staff engineer, e-commerce migration post-mortem
What you should actually do: before you write the first line of migration code, define a sunset date for every shim and every parallel system. Make that date a deploy block—if the shim still exists, the pipeline fails. Build a single dashboard that shows the delta between old and new systems for every data flow. If the delta is non-zero for two consecutive quarters, escalate it as a risk to leadership. And for the love of shipping, don't let anyone write a new feature on the old system after the migration starts. That's how you double your maintenance burden overnight. The cost of running two systems isn't two times the original cost—it's four, because every decision requires context, every deploy requires coordination, and every new hire needs to learn two histories instead of one. That's the cost nobody calculates, and it's the one that kills projects quietly.
When Migrations Are a Bad Idea (and What to Do Instead)
The static brochure site that works fine in jQuery
I once walked into a team sweating over a twelve-year-old restaurant website. It loaded in under a second, had zero login screens, and generated a steady stream of weekend reservations. The lead developer wanted to migrate it from jQuery to React — because, and I quote, 'jQuery is dead.' That site had three pages, a contact form, and a Google Maps embed. The migration would have taken two weeks and solved exactly zero customer problems. The catch? The team wasted four days building a component tree before someone asked what business outcome they were chasing. A static site that works is not technical debt. It's a running car. Don't rebuild the engine because the hood looks old.
Here's the litmus test: can the current framework serve users reliably for another two years with minimal upkeep? If yes, migration is vanity. You might argue that skills atrophy matters — but I have seen teams rewrite a brochure site and then discover their new React bundle is three times heavier than the original jQuery file. The only thing you achieve is slower load times and a ticket backlog for 'migration cleanup.' That sounds fine until your CTO asks why the blog now needs a build pipeline.
Reality check: name the frameworks owner or stop.
The internal tool with three users — don't touch it
Internal tools are the graveyard of costly migrations. A logistics company once asked me to help port their warehouse inventory app from AngularJS to Svelte. Total active users? Eleven. Three of them accessed the app daily. The AngularJS version had two known bugs, both with workarounds documented in a shared Notion page. The migration estimate came back at six weeks. That's six weeks of not building the feature the warehouse managers actually wanted: barcode scanning on a tablet. The pitfall is obvious once you say it aloud: you're trading user value for architectural purity.
'We migrated an internal dashboard for nine people. The old framework was unmaintained, but the new one introduced a memory leak that took three months to reproduce.'
— Lead engineer, mid-size logistics firm, after a hard-won revert
Most teams skip this: an internal tool with a small user base has no meaningful scaling pressure. The maintenance burden is human labor, not framework decay. If one developer can patch the old code twice a year and everyone stays productive, that's cheaper than any migration. Refactor only the parts that actively hurt — the report generator that crashes, the search that times out. Leave the rest alone.
When refactoring the old framework is cheaper than switching
The most honest conversation I ever heard in a tech lead meeting: 'We don't have a framework problem. We have a spaghetti-registration-form problem.' The migration was being pitched as a solution for bad code architecture — which it rarely is. What usually breaks first is not the framework's rendering engine, but the tangled module that grew through five years of unchecked feature adds. You can slap any new framework on that mess and it will still be a mess, just slower.
The trade-off here is surgical versus radical. A targeted refactor of the worst 20% of the codebase typically costs one-third of a full migration and produces the same stability gains. I have seen teams gut a single 2,000-line PHP controller, replace it with two small services, and reduce bug reports by 40% — all while keeping the old Laravel version running. That's not glamorous. It doesn't generate a conference talk. But it keeps the business running while the team builds actual features. If your migration plan doesn't include a hard 'no' against any framework swap that can't beat a 40% bug-reduction number on paper, you're betting on hope — and hope doesn't reimburse the wasted month.
Here is your next action before writing a single line of new code: map every user-facing bug in the current system to its root cause. If fewer than half trace back to framework limitations, cancel the migration. Refactor the dirty modules instead. You will thank yourself when next quarter's feature ship date arrives intact.
Open Questions and FAQ
How do we know when a migration is actually 'done'?
The honest answer is: you never really finish—you just reach a point where the old system's ghost stops haunting your standups. I've seen teams celebrate a "cutover" only to discover six months later that the old codebase is still running in a corner, serving a few forgotten API consumers. Real done-ness looks like: zero traffic on the legacy system for at least two full deploy cycles, all team members can ship features entirely in the new framework without touching the old one, and your incident dashboard doesn't spike after every release. That sounds clean. The catch is—getting there usually means letting go of the idea that "done" is a date on a calendar. It's a set of verifiable conditions, and most teams stop measuring too early.
What if our team doesn't know the new framework yet?
You learn it on the job—but you have to budget for the learning curve like you'd budget for a new hire's ramp-up. The mistake I keep seeing is leadership assuming a three-day workshop makes a senior engineer productive in React 18 or Svelte 5. It doesn't. Plan for a 30–40% velocity drop for the first sprint, maybe two. One concrete approach: pair a junior who's eager to learn the new stack with a senior who knows the business logic cold but is also learning the framework. They'll make slow, ugly progress. That's fine. The alternative—forcing the whole team to "upskill first" for a month—usually kills momentum because nobody's shipping anything. What actually works is a single, low-stakes module migrated first, with explicit permission to rewrite it badly. You'll refactor later. Honestly, if you don't plan for the bad code, you'll never ship the good code.
“We let three devs spend two weeks on a tutorial before touching production. Day one of real migration, they couldn't debug a broken component. Day fourteen, they were fine. But we'd lost thirteen days of trust.”
— tech lead, mid-size B2B SaaS, after a React-to-Vue migration
Can we migrate without stopping feature development?
Yes—if you treat the migration as its own feature track, not a side project stolen from Fridays. The trick is carving out a "strangle" pattern: every new piece of business logic gets built in the new framework from day one, while the old system only gets bug fixes and critical patches. That means your product team must accept a temporary split in the codebase. It's ugly. It violates every clean-architecture instinct you have. But the alternative—freezing features for three months—gets the migration killed by stakeholders who need revenue. What usually breaks first is the strangler fig routing layer. You'll need a proxy or feature flag system that can route requests to whichever backend handles that endpoint, and that layer will accumulate technical debt fast. Plan to sunset it within six weeks of full cutover, or it becomes its own legacy system. Most teams skip this: write a one-page runbook for that router the day you deploy it, not six months later.
How do we measure success before the pain hits?
Pick three metrics—no more—and track them weekly. I'd suggest: time-to-deploy for a single change, incident count per sprint, and developer-reported confidence (a simple 1–5 survey after each release). If time-to-deploy drops, you're winning. If incidents spike, you're cutting corners. If confidence stays below 3 for three sprints, the migration is burning morale. The most common failure mode is measuring only technical outcomes—lines migrated, services converted—and ignoring the human cost. A team that ships bad code fast is not ahead. They're building a pile of rubble that someone else will have to dig through. Set a hard rule: if any of those three metrics worsens for two consecutive sprints, pause the migration for a week. Fix what hurt. Then resume. That doesn't make you slow—it makes you honest.
Summary and Next Experiments
Write down three anti-patterns your team is about to commit
Stop reading. Grab a sticky note—right now. List the three migration mistakes lurking in your next sprint. I've watched teams burn weeks on "we'll just dual-run both systems for a month" without ever planning the cut-over date. That's not pragmatism; that's avoidance. Another classic: rewriting business logic while porting the framework. You're now debugging two unknowns simultaneously. The third? Letting the old codebase keep accepting new features during the migration. You'll never finish if the target moves every Tuesday.
Your list doesn't need to be perfect. It needs to be honest. One team I worked with admitted their "strangler fig" pattern was strangling them—the monolith grew over three years, they'd migrated six modules, and nobody remembered which framework ran the billing service. That hurts. So write the damn list. Then tape it to your monitor. Next time someone says "let's just add one more endpoint to the old system," you've got a visual veto.
Run a one-week spike with the new framework on a real feature
Not a hello-world todo app. Not a refactored login page. Pick a feature that touches real data—a search endpoint, a report generator, something that screams when you mess up. Give yourself five days to get it working in the new framework, with actual tests, deployed to a staging environment that mirrors production load.
The catch: you must throw out the spike code afterward. No merging. No "we'll clean it up later." The goal is raw learning—where do the configuration traps hide? How long does a simple query actually take? What breaks when your auth middleware meets the new routing layer? I've seen teams discover on week three that their ORM of choice has a nested-transaction bug that only surfaces under PostgreSQL 15. Find that on day three, when the cost of walking away is still zero.
You'll either validate the migration path or uncover a deal-breaker. Both outcomes are cheaper than a stalled six-month rewrite. Both save your team from the sunk-cost spiral that kills projects.
“We spent four months migrating. Then we spent six months untangling the broken auth flows we introduced. The old system was untouched the whole time—it would have been faster to start from scratch.”
— Anonymous lead engineer, post-mortem for a failed Rails-to-Next.js migration
Calculate the cost of running two frameworks for six months
That dual-runtime phase you're planning? It's not free. It's not neutral. It's a tax you pay every sprint. Calculate it before you commit: developer context-switching overhead (each engineer loses 20–40 minutes per framework flip), CI pipeline duplication (two build systems, two test suites, two dependency graphs), and the silent killer—knowledge fragmentation. Half the team learns the new stack; the other half maintains the old. Nobody becomes expert in either.
Run the numbers for your actual team size. Six months of two frameworks might mean three delayed features, one burned-out senior dev, and a mounting technical debt that nobody accounts for in the migration budget. If the cost exceeds 40% of the migration effort itself, you need a faster cut-over strategy. Or a hard stop: kill the old system by a fixed date, no exceptions. That's not reckless—it's honest about the scarcity of your team's attention. You only get one focus at a time. Choose which framework actually deserves it.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!