Logo von nextlevels
DEEN
Request a project

Refactoring

Last updated on

In short

Refactoring refers to the deliberate change of the internal structure of software without changing its externally visible behaviour. The code becomes more readable, easier to extend and cheaper to maintain, while functions, interfaces and results stay exactly the same for users and connected systems.

The term was shaped by Martin Fowler with his book "Refactoring: Improving the Design of Existing Code", which appeared in 1999 and was republished in a second edition in 2018.

For managing directors and heads of IT, refactoring is above all an investment decision. It costs developer time but delivers no new feature that can be shown to sales. This entry explains what refactoring covers in concrete terms, what prerequisites it needs, how it differs from a rewrite and from modernisation, when it is enough and when it is not, and what it brings economically.

What refactoring is and what it is not

The definition has two hard criteria. First: the behaviour stays the same. A refactoring adds no function, fixes no functional bug and changes no output. Second: the structure changes. Names become more precise, functions smaller, dependencies clearer, duplicates disappear. Fowler therefore describes refactoring as a sequence of many small, behaviour-preserving changes, each of which is almost trivial on its own. The effect comes from the sum.

This narrow definition matters because in day-to-day project work the term is often used as a catch-all for "let's tidy up a bit". Anyone who builds in a feature and fixes a bug while tidying up is no longer refactoring but doing three things at once. That is exactly what makes errors hard to find if something does not work afterwards. The clean separation is not pedantry but risk management.

A widespread misunderstanding also concerns timing. Refactoring is not a special phase scheduled every two years but part of normal development work. Fowler calls this the "two hats" rule: either you are currently wearing the "add function" hat or the "improve structure" hat, but never both at once. You may switch between the hats as often as you like, even several times an hour.

Typical refactorings

Fowler's catalogue, which is also maintained online at refactoring.com, comprises around seventy named changes in the second edition. The most important ones can be explained in a few sentences. Extract Function pulls a block of code out of a long function and gives it a meaningful name; the long function thus becomes a readable sequence of steps. Rename changes the name of a variable, function or class so that it expresses what the thing actually does. That sounds trivial, but it is one of the most effective refactorings of all, because code is read far more often than it is written. Move Function or Move Field moves logic to where the data it works with lives, thereby reducing dependencies between modules.

Beyond the classic catalogue, technical maintenance work that preserves behaviour but modernises the foundation also counts in practice. Dependency upgrades raise libraries and frameworks to current versions, close security gaps and keep access to new features open. For mobile apps there is also raising the target SDK: for store submissions, Google and Apple regularly require apps to be built against a current Android or iOS version. If you miss this, at some point you can no longer publish updates. Such work delivers nothing visible to the user, but it is the precondition for the app staying in the store and for the team being able to make changes safely at all.

Prerequisites: tests, version control, small steps

Refactoring without a safety net is gambling. The most important net is automated tests. After every change they prove that the behaviour is unchanged. Without them, the basic condition "behaviour stays the same" simply cannot be checked, and every change becomes a test of courage. Fowler is unambiguous here: the first step in an untested legacy system is not refactoring but writing tests that pin down the current behaviour, even if that behaviour contains bugs. Such characterisation tests are not pretty, but they make every subsequent step verifiable.

The second prerequisite is version control with small commits. A refactoring that changes fifty files in one go can hardly be assessed in review and cannot be cleanly reversed if something goes wrong. Small, named steps such as "Rename OrderService.process to OrderService.submit" are traceable and can be rolled back individually. The third prerequisite follows from this: discipline about step size. Fowler's mechanics consist of compiling and testing after every mini-change. If you keep this up, you find errors within minutes instead of days.

On top of this comes tool support. Modern IDEs carry out rename, extract and move automatically and across all usages, which drastically reduces the error rate. Static analysis shows where complexity and duplicates sit. A continuous integration pipeline that runs the tests on every push makes the safety net visible to the whole team, not just to the developer at the keyboard.

Distinction: refactoring, rewrite, modernisation

The terms are often mixed up in proposals and decision papers, but they describe very different undertakings with different risks. Refactoring works in the existing code and preserves behaviour. A rewrite builds the system from scratch, usually with a different technology, and in the end replaces the old one completely. Modernisation is the umbrella term for all measures that make a legacy system fit for the future; it can include refactoring, but also migrations, container operation or splitting off individual services. Replatforming, in turn, changes the platform under the system, for example from one shop system to another, and is therefore closer to a rewrite than to refactoring.

Refactoring, rewrite and modernisation compared
CriterionRefactoringRewriteModernisation
External behaviourStays identicalIs redefined, often with deviationsUsually stays the same, can be extended
Code baseExisting code is restructuredNew code replaces the oldMixture: some parts stay, some are replaced
Typical durationHours to weeks, ongoingMonths to yearsQuarters, in stages
RiskLow if tests existHigh: parallel operation, feature gaps, budgetMedium, depending on the size of the cut
Benefit for the businessIndirect: faster changesDirectly visible, but lateVisible step by step
Typical triggerEvery change, code smellsTechnology at end of life, team without know-howOperating costs, security, compliance

The decisive difference lies in the risk. A rewrite promises a clean system but requires months of parallel operation in which the old system is maintained and the new one built. Joel Spolsky described this years ago as the single worst strategic mistake a software company can make, because the legacy system contains knowledge that nobody knows any more: special cases, workarounds, agreements with customers. Refactoring preserves this knowledge because the code is never thrown away. The consequence for practice: most systems for which a rewrite is demanded would first need tests and a few weeks of consistent refactoring. Only if that does not help is a rebuild an honest option.

When refactoring is enough and when it is not

Refactoring is enough as long as three conditions are met. The framework or platform continues to be maintained, receives security updates and has a path to the next major version. The team knows the code or can get to know it with reasonable effort, and it feels confident enough to make changes. And the basic architecture fits the functional requirements of the coming years; it may be messy, but it is not wrong.

If one of these conditions tips over, refactoring reaches its limits. A discontinued framework cannot be rescued with cleaner function names; a migration is due here, which can run in small, refactoring-like steps but has a different objective. If the team lacks any confidence because the original developer has left and no test exists, the test basis has to be built first before any change is justifiable. And if the architecture fundamentally does not hold, for example a monolith that is supposed to scale to multi-tenancy or ten times the load, then local restructuring helps little. Then it is about decomposition, for example gradually splitting off services, or about a targeted partial rewrite.

A useful test for the decision is the question of technical debt: is it local or structural? Local debt sits in individual modules that are too big, too nested or badly named. It can be paid down with refactoring. Structural debt sits in decisions that run through the whole system, such as a data model that maps the business domain wrongly, or a dependency from which nothing can be separated. It requires more than refactoring. With custom software this distinction is particularly relevant, because there nobody except your own team or your service provider can make the decision.

Costs and benefits

The costs of neglected refactoring can be quantified. In a developer survey published by SonarSource under the title "Developers spend 30% of their time on code maintenance", professional developers stated that on average they spend around 30 per cent of their working week on code maintenance, about 12 hours a week. The range was from 11 to 50 per cent; larger teams were above average, presumably because they work with older and larger code bases. Around a quarter of maintenance time went on open-source dependencies, and the most time-consuming single task was switching to a new major version of a framework or library. The survey also cites a Stripe report that puts the figure as high as 17.3 hours a week for maintenance and bad code.

For a team of five developers, a 30 per cent share of maintenance means, arithmetically, one and a half full-time positions not working on new functions. Every percentage point that refactoring takes out of this block is either feature capacity gained or personnel costs saved. The benefit, however, does not show in the week of the change but in the months afterwards: changes take less time, onboarding new developers is faster, the error rate falls. That is exactly why refactoring needs a sponsor at management level who accepts this delay.

At the same time there is refactoring that does not pay off. Code that has been running unchanged for years and will foreseeably not be touched does not need restructuring, even if it is ugly. Fowler recommends opportunistic refactoring: you restructure where work is going on anyway or where the next change is due. The priority follows the frequency of change, not aesthetic sensibilities.

Real-world example: the mandatory migration to the React Native New Architecture

What an enforced, large-scale refactoring looks like is currently being shown very concretely in the mobile world. On 8 October 2025 Meta released React Native 0.82, the first version that runs exclusively on the so-called New Architecture. The old bridge between JavaScript and native code can no longer be activated since then; the configuration switches with which the Legacy Architecture could previously be enforced are ignored. In its guide to the New Architecture, the Expo team documents that Expo SDK 54 is the last version in which the old architecture can still be switched off, and that SDK 55 runs entirely on React Native 0.83 and therefore only on the New Architecture. According to Expo, around 83 per cent of the SDK 54 projects built with EAS Build were already using the New Architecture in January 2026.

For companies with a React Native app, this is a classic behaviour-preserving refactoring: the app is supposed to work exactly the same for users but be switched internally to a new runtime. The recommended approach follows Fowler's mechanics exactly. First upgrade to React Native 0.81 or Expo SDK 54, switch on the New Architecture there, test and fix every deviation, identify and replace incompatible libraries via the React Native Directory, and only then move to 0.82. Skip the steps and errors can no longer be localised, because too much was changed at once. For a native app without a JavaScript layer, the same logic applies to every target SDK upgrade or to major versions of Swift and Kotlin. If you want support with such a migration, our app development has teams that carry out exactly these conversions regularly.

The example also shows why refactoring cannot be postponed indefinitely. Expo has frozen the Legacy Architecture; it receives neither new functions nor bug fixes, and many widely used libraries only support the new variant. If you wait, you still pay for the migration later, just with more dependencies breaking at the same time. In such cases technical debt has a fixed due date.

Frequently asked questions

How do I convince management of refactoring if it delivers no feature?

With figures from your own company. Measure how long comparable changes take in clean and in neglected modules, how many production errors come from which part of the system and how much time the team spends on maintenance instead of development. The survey values of around 30 per cent maintenance share are a useful starting point; your own figure is more convincing. Also, do not sell refactoring as a project but as part of every change that is already included in the estimate. Then there is no separate item that can be cut.

Can refactoring cause errors?

Yes, if it happens without tests or in steps that are too large. The method itself is designed for safety: small, behaviour-preserving changes with a test run after every step. Errors arise when functions are changed at the same time, when tests are missing or when a "refactoring" is in truth half a rewrite. Another risk factor is interfaces to the outside, such as a public API: a rename there is no longer refactoring because it changes the behaviour for consumers. Such places need a transition phase with the old and the new signature.

How much time should a team plan for refactoring?

A fixed quota is less helpful than a rule. Fowler's recommendation is to couple refactoring to ongoing work: whoever changes a module leaves it slightly better than they found it. This is included in the estimate for a task, not shown separately. In addition, a small, deliberately prioritised allowance for dependency upgrades and platform maintenance is worthwhile, such as target SDK upgrades for apps, because this work has deadlines set from outside. A guideline from practice is 10 to 20 per cent of capacity; more important than the figure is that the item is visible in the sprint and is not the first thing cut at every bottleneck.

Next step

Refactoring, strangler or rebuild?

Six questions, two minutes: you get a risk score for your legacy system and the right path, whether refactoring, strangler or rebuild.

Changelog

  1. · Created · Jan Schumann