Pedro Sousa
← Articles

Article · Blog

You didn't refactor the code. You just moved the problem somewhere else.

Refactoring is not reorganizing code. It's changing internal behavior without changing external behavior, and most refactors that happen in real projects don't do that. They just relocate where the problem lives.

You didn't refactor the code. You just moved the problem somewhere else.

In a financial app I worked on, the statement screen had a ViewController with 900 lines. Everyone knew it was wrong. Everyone agreed it needed refactoring.

Three months later, the ViewController had 200 lines. But the project had a TransactionViewModel with 700 lines, a TransactionUseCase with 400 lines, and a TransactionRepository that parsed JSON, handled network errors, and still applied business rules.

The code was "clean." The architecture was "correct." And the original problem was intact, just redistributed across more files.

That's not refactoring. That's reorganization with new naming.


The illusion of the right structure

Most refactors I've seen over 21 years of working in this field solve the wrong problem.

The developer sees a 150-line method and extracts it into 4 smaller methods. Each method now has 30 lines. Did the code get more readable? Maybe. Was the problem solved? Depends on what the problem was.

If the problem was mixed responsibilities, extracting methods without redefining responsibilities only creates smaller methods with responsibilities that are still mixed.

If the problem was tight coupling, moving code to another file without breaking the dependency just hides the coupling behind a layer of indirection.

Clean Architecture doesn't solve coupling. It offers a structure where coupling is easier to avoid. The difference matters.

I fell into this trap too. At XP, when the codebase grew alongside the customer base, the reflex was to create layers. Use case here, repository there, mapper in between. The structure looked great. But some dependencies crossed layers in ways the architecture wasn't supposed to allow, and nobody noticed because all the files were in the right places.


The real problem behind most bad refactors

Refactoring requires that you understand what the code is actually doing, not what the method name says it does.

That's the step most people skip.

It's faster to look at a bloated ViewController, extract the apparent responsibilities into separate classes, and declare it done. It's slower to sit with the code, understand every decision that was made, figure out why that bizarre logic is there, and only then decide what to move and what to rewrite.

The difference between the two is the difference between a refactor that lasts and a refactor that creates the next problem.

At OLX, when I took over the Account module to raise test coverage from 65% to 87%, my first instinct was to reorganize. New structure, smaller files, everything neatly separated. But what actually made the tests work was understanding which parts of the code had implicit state that nobody had documented. Reorganizing without addressing that would have produced tests that tested the structure, not the behavior.


The pattern I call Cosmetic Refactoring

You recognize it by the symptom: the pull request has 40 modified files, the description says "cleanup and organization," the tests keep passing because no behavior actually changed, and three weeks later the next developer opens a bug that existed before the refactor.

Cosmetic Refactoring is when you change what the code looks like without changing what the code does.

It's seductive because it's immediate. You see results. The diff looks impressive. The code gets more readable. But the design problem, the one that justified the refactor in the first place, is still alive.

The clearest sign of Cosmetic Refactoring is when, after the refactor, you still can't test a unit of logic without instantiating half the system. Testability is the most honest proxy for design. If the design improved, testing got easier. If testability didn't change, the design didn't change.


What actually works in practice

The most effective decision I learned to make before any refactor is simple: write the test you want to be able to write afterward.

Not the test you can write today. The test that would describe the behavior that unit should have, in isolation, without any dependency on infrastructure.

If you can't even write that test before refactoring, you don't yet understand the problem well enough to solve it.

This looks like TDD but it's not exactly TDD. It's using the test as a design tool before touching production code. The test reveals coupling that reading the code hides.

In practice, what lasted in the projects I've been part of was refactoring in small, verifiable steps. Not rewriting, not mass reorganization. One isolated behavior at a time, with a test covering it before moving anything.

The refactor I took most seriously in my career was during a VIPER to MVVM migration in a reputation module at OLX. What saved the migration was having behavioral tests in place before starting. They told me if something had broken, not if the structure looked nice.


The honest trade-off

Refactoring slowly, with tests, in small steps, is slower in the short term. Sometimes a lot slower.

On a team under delivery pressure, spending an entire sprint on refactoring with no new feature is hard to justify. And sometimes Cosmetic Refactoring is the right call because the code is going to be thrown away in six months anyway.

The problem is when you apply Cosmetic Refactoring to code that's going to last for years, and call it an architecture improvement.

The decision that survives over time isn't the one that uses the most sophisticated pattern. It's the one that solves the right problem with the minimum necessary complexity.

I've seen systems with impeccable Clean Architecture that were impossible to maintain because every new feature required touching six layers. And I've seen code with no architecture name attached to it that survived ten years because the dependencies were under control.


What I would do differently today

When I was younger, I refactored for structure. I moved code to the architecturally correct places and thought I was done.

Today I refactor for testability and changeability. The question isn't "is this code in the right place?" The question is "if this behavior needs to change tomorrow, how many files do I need to open?"

If the answer is more than two or three, the design has a problem, regardless of how many layers are properly named.


Most refactors don't fail because the developer picked the wrong pattern. They fail because the developer reorganized what was visible without solving what was invisible.

Clean code is a consequence of dependencies being under control. Not the other way around.

And every time you finish a refactor without being able to test a unit of logic in isolation... the problem is still there. It just lives at a different address now.