Not every messy codebase needs a rewrite. But some reach a point where incremental fixes cost more than a structured migration. Here are five signs you're there.
1. Simple features take disproportionately long
When adding a basic CRUD page or form takes days instead of hours because you have to navigate inconsistent patterns, work around architectural limitations, or fix unrelated regressions — your architecture is actively slowing you down.
Measure it: Track time-to-ship for comparable features over the last 6 months. If it's increasing, that's a signal.
2. No one person understands the full system
When knowledge is siloed — "only Sarah knows how the checkout flow works" — and Sarah leaving would be a crisis, your architecture has too many implicit dependencies and not enough documented patterns.
Measure it: Can a new engineer make a safe change to any part of the frontend within their first month?
3. Performance degrades with every release
If bundle size grows monotonically, Core Web Vitals decline quarter over quarter, and every new feature requires a performance exception — the foundation can't support the product's growth.
Measure it: Track bundle size and LCP/INP trends over 6 months. Consistent degradation is a structural problem.
4. Three or more state management patterns coexist
Redux in one area, React Context in another, Zustand in a third, local state everywhere else. When there's no consistent data fetching or state management strategy, every feature reinvents the wheel.
Measure it: Count the number of distinct patterns for data fetching and state. More than two is a smell.
5. Tests are either absent or constantly breaking
If your test suite doesn't give confidence — because tests are flaky, outdated, or cover so little that passing tests don't mean the app works — you can't refactor safely. And without safe refactoring, you're stuck.
Measure it: What percentage of PRs introduce regressions that tests should have caught?
What to do instead of a big-bang rewrite
A full rewrite is almost always the wrong answer. Instead:
- Audit and document current architecture, pain points, and constraints
- Define target architecture with clear patterns and ADRs
- Migrate incrementally — start with leaf pages/features, not core flows
- Establish the new pattern on new features first
- Set a performance budget and enforce it in CI
- Measure velocity — are features shipping faster after each migration phase?
The goal isn't a perfect codebase. It's an architecture that lets your team ship reliable features at increasing speed.