Every consultancy will tell you "it depends." That's true and also not very useful. Here's the actual test we use, illustrated with two real projects that looked similar on the surface and needed completely different fixes.
The test: is the constraint architectural, or is it implementation?
If the core data model and system architecture are sound, and the problems are in specific modules — bad queries, missing indexes, sloppy error handling, outdated dependencies — that's implementation debt. Refactor it. The foundation is fine; the details on top of it aren't.
If the constraint is baked into the platform or architecture itself — a data model that can't represent what the business now needs, or a platform whose rules block a requirement no amount of clever engineering can work around — that's architectural debt, and no amount of refactoring fixes it. That needs a rebuild, or at minimum a platform change.
Case one: refactor
An equipment marketplace client was running a Laravel application managing 30,000+ pieces of equipment against 200,000+ related parts — deep, genuinely complex relational data. Pages were slow, admin tooling dragged, and the standing fix had been to keep scaling up the database instance. When we looked at it, the architecture was fine. Laravel and a relational database are the right tool for this kind of data. The problem was in the implementation: missing indexes, N+1 queries, some denormalization needed in a few specific hot paths. We fixed the query layer and the data relationships without touching the overall architecture, and the same infrastructure that used to strain now runs comfortably. A rebuild here would have been expensive and pointless — the foundation wasn't the problem.
Case two: rebuild (platform change)
A different client was running on Shopify Plus and needed tiered B2B pricing, quote-to-order workflows, net-terms accounts, and deep ERP integration for order and inventory sync. Some of this is achievable on Shopify Plus through its B2B tools and apps. But past a certain point of customization, you're not refactoring anything — you're working around limits that are part of the platform by design, stacking apps and fees to approximate functionality a general-purpose ecommerce platform wasn't built to natively support at this level of specificity. No amount of clever Liquid or app configuration changes that. We rebuilt the commerce layer on a self-hosted Laravel Lunar stack, which gave the client full control over pricing logic, order workflows, and ERP integration, and dropped their monthly infrastructure cost from about $2,400 to $150 in the process. That wasn't a refactor — it was a platform-level constraint that required a genuine rebuild to remove.
How to tell which one you're looking at
Ask what's actually blocking you. If the answer is "the code is messy, slow, or hard to maintain," that's implementation debt — refactor it, ideally before it gets worse, because refactors get more expensive the longer messy code accumulates more messy code around it. If the answer is "the platform or architecture fundamentally can't do what the business needs," no amount of refactoring closes that gap, and it's worth being honest about that early rather than spending six months of engineering time trying to route around a wall that isn't going to move.
The expensive mistake in both directions is the same: rebuilding something that only needed a refactor wastes money on a clean-slate project you didn't need, and refactoring something that needed a rebuild means eighteen months of incremental fixes that still end with the business blocked by the same wall it started with.