A high-change application is constrained by its structure. What distinguishes refactoring from re-architecting here, and how do you choose?
Show the full answer Hide the answer
The distinction
Refactoring changes internal structure without changing external behaviour or boundaries. Extracting modules, removing duplication, improving names, separating concerns. Reversible, incremental, and it can proceed alongside feature work.
Re-architecting changes the boundaries: splitting a component into services, changing the data model, altering the deployment unit, moving from synchronous to asynchronous. Expensive, harder to reverse, and it usually requires a migration.
How to choose
Refactor first, almost always. Two reasons, and the second matters more:
- It is cheaper and reversible.
- It reveals where the real boundaries are. Modules that stabilise and stop changing together are candidates for extraction; modules that keep changing together are evidence that the proposed boundary is wrong. Extracting a service before that evidence exists means guessing, and a wrong service boundary is far more expensive to fix than a wrong module boundary.
Re-architect when refactoring cannot deliver the required property. Specifically:
- Independent deployment is required because release coupling is the binding constraint.
- Independent scaling is required because one subsystem needs materially more resource than the rest — and in that case extract only the bottleneck, not everything.
- Different availability or failure semantics are required for one part.
- A different data model is required, which refactoring cannot reach.
The intermediate step teams skip
A modular monolith with enforced internal boundaries. Static checks on dependency direction, clear module ownership, and no reaching into another module's internals. This delivers most of the change-cost benefit of decomposition without the distributed-systems cost — and it produces exactly the evidence needed to decide which boundaries are worth making into services.
Teams that skip it are choosing service boundaries from a design diagram rather than from observed change patterns, which is why so many decompositions produce services that must be deployed together.
The test before extracting a service
Has this module changed independently of its neighbours for a meaningful period? If not, extracting it produces a distributed component that still requires coordinated deployment — the distributed monolith, which has the costs of both approaches and the benefits of neither.