PaxLee
PaxLee学无止境
Back to list
Don't Refactor Because Code Is Ugly—Refactor Because Business Hurts
App开发工程实践架构决策技术债务重构时机小团队

Don't Refactor Because Code Is Ugly—Refactor Because Business Hurts

Published July 18, 20266 min read

Refactoring is tempting when code gets messy, but small teams can't afford blind rewrites. This article offers a pain-driven decision framework and gradual refactoring steps based on real business bottlenecks.

The urge to refactor is universal, but before you touch a line, ask: where does it hurt?

In my early days of building apps, every few months I'd hit a wall: adding a small feature took forever, fixing a bug meant spelunking through tangled code. I'd slam the table and declare "refactoring." Two weeks later, a shiny new module would go live—only to crash on users' devices, followed by two more weeks of regression fixes. And the original pain? Maybe 60% better.

I've since learned one thing: messy code alone is not a reason to refactor. Business pain is.

Small teams don't have the luxury of quarterly architecture budgets. Every refactor is a bet—risking current stability for future velocity. The decision shouldn't be based on code aesthetics, but on whether your architecture is genuinely blocking feature delivery.

What kind of pain are you feeling?

I've been through a few typical scenarios, each requiring a different response:

1. Exponential growth in incremental feature cost

Track the actual time spent on your last five small features (e.g., adding a setting option, changing sort order). If the average time has tripled or more compared to the project's early days, and each change touches 5+ files or classes, your architecture is dragging you down. But differentiate: is it poor module boundaries, or just messy code style? The former needs structural change, the latter just linting and conventions.

2. Changing A always breaks B

This is tight coupling. If you've had two incidents where modifying payment flow broke login, your boundaries are wrong. Even if features are shipping on time, prioritize dependency decoupling.

3. New team members spend more time understanding code than business logic

Small teams rarely hire, but when they do, the new person's face tells you everything. If they need days to trace navigation flows and class inheritance before grasping the product, your architecture lacks clarity.

4. Build/compile time becomes a distraction

Waiting 2 minutes for a simple UI tweak kills a small team's flow. But slow compilation isn't always an architecture problem—it could be dependency management or lack of modularization. Extract modules first before considering a full rewrite.

A simple decision matrix

I use these five questions. The more "yes" answers, the higher the urgency:

  1. Is average modification time for the module > 2.5× the historical average?
  2. Have there been 2+ cross-module regression bugs in the past month?
  3. Is only one person on the team able to maintain or understand this code?
  4. Is there a clear feature demand that is blocked by code coupling?
  5. Has the build/test cycle become a bottleneck in daily development?

If 3+ are "yes," consider refactoring. But if only 1–2, I'd lean toward localized improvements or adding tests instead of major surgery.

Gradual refactoring: the only way for small teams

I've failed enough to know that big-bang rewrites are a trap for small teams. Here's my approach:

Step 1: Draw a moat. Write integration tests for the core paths of the module you're about to touch. Even a simple UI smoke test is better than diving in blind.

Step 2: Extract, don't rewrite. Pull out the most volatile sub-feature into a separate module while keeping the interface identical. For example, I once had recommendation logic baked into an Activity. I extracted it into a standalone Presenter/ViewModel with the same public API. All 20 call sites stayed untouched, reducing risk.

Step 3: Replace in batches. Don't refactor an entire module at once. Sort sub-components by modification frequency—the most frequently changed parts get priority. You may never need to touch low-change code.

Step 4: Gray release. Use a feature flag to expose the new code to a small set of users. If crash rate spikes, flip back instantly.

Beware of fake pain points

Sometimes you feel the code is messy, but the real issue is rapid requirement changes—not bad architecture. If your product is still in PMF validation mode and pivoting weekly, messy code is normal. Refactoring at that stage is like building a castle on sand.

Another fake pain: "Someone else's code is terrible." Every developer has written bad code, but if you can read it, understand it, and tests cover it, it's deliverable. Ask yourself: if you had written it yourself, would you still want to refactor?

A hypothetical case

Imagine I'm building a note-taking app. MVP used a single-Activity-multi-Fragment structure with network calls inside Fragments. Six months later, users are growing, and demands pile up—tags, sharing, offline mode. Changing one Fragment starts breaking others because a utility class is heavily coupled.

I measured the last task (adding tag filtering): it went from 4 hours to 10 hours. The change involved 8 files, 3 of which were unrelated because they shared a network callback.

Decision matrix: ① yes (2.5×), ② yes (had incidents), ③ no (both teammates understood it), ④ yes (offline feature was blocked), ⑤ no (build time okay).

Three "yes" — I decided to refactor. But only the network and storage layers. Switched from raw HTTP calls to Retrofit + Repository pattern, extracted local database. UI layer only needed a few call-point adjustments. The refactor took two weeks, followed by one week of gray rollout. Crash rate stayed stable, and subsequent feature velocity recovered about 60%.

Had I gone for a full UI rewrite, it would have taken a month longer and carried far more risk.

What happens after the refactor?

Many think refactoring is the finish line. It's not. Without code review and conventions, the codebase will degrade again within six months. Small teams don't need formal review cycles, but enforce two rules in PRs: changes under 400 lines and at least a basic test.

Also, don't promise immediate feature acceleration after refactoring. Reserve 1–2 iterations to absorb the learning curve and regression testing.

Final thoughts

Architecture evolution is not a tech competition—it's a business decision. For small teams, the most precious asset isn't clean code, but speed of response to market changes. Refactoring should serve that speed, not undermine it.

If you're hesitating about a messy codebase, open your commit log and calculate the real cost. If the data says it hurts, make a localized incision. If it doesn't, keep running.

PaxLee