No Refactoring Issues

The inevitable cleanup, and how not to do it.

Nico Rehwaldt 2025

πŸ“ Exercise your own judgement as you digest the contents of this presentation.

Understanding accidental and essential complexity in software engineering.

Essential (inherent) complexity is caused by the problem to be solved, and cannot be removed.

Accidental complexity is what engineers create and can fix.

Adapted from No silver bullet

Accidental complexity

Accidental complexity has many forms, in code and UX

A quick fix enables interaction A. As a result, the user journey B is broken.

We plan to do X as a first step towards a higher goal, but never follow up.

Accidental complexity is often caused at design time, by insufficient understanding of the business domain.

πŸ’¬ "Make this button red!"

❓ "Should the user be able to execute this irreversible operation?"

[Most software] breaks because teams lose alignment with the business problem they’re supposed to solve. Systems become tangled with technical assumptions that age poorly (ref).

Accidental complexity is inevitable

Environments change, requirements change, rendering functionality outdated and dependencies vulnerable.

Over time any application decays in code and user experience, whether you touch it or not.

We introduce accidental complexity all the time, through leaky abstractions, half-baked trade-offs, and "quick fixes".

We want to have measures to keep our applications in check.

Technical debt = accidental complexity, in code

[Technical debt is] a collection of design or implementation constructs that make future changes more costly or impossible (ref).

essential complexity and cruft, as described by Martin Fowler

How to tame it? Through refactoring.

Refactoring is a [...] technique for restructuring an existing body of code, altering its internal structure without changing its external behavior (ref).

The goal of refactoring is to adapt existing code to make new changes straightforward.

Refactoring ensures that the application, over time, stays fit for purpose, for its use.

A graph showing the velocity of changes decreasing over time, if no refactoring is applied
A graph highlighting the actual "work done", if no refactoring is applied.

Refactoring keeps the velocity in check.

By doing it continuously, we keep the effort low.

By doing it continuously, we make it a habit, a normal thing, an inherent part of our developer tool belt. We make it a path traveled.

Two paths, one is less traveled

By doing it opportunistically, focusing on the current task at hand, we ensure that we do it where it matters most: in critical code paths.

Rules of (not) Refactoring

Our environment shapes us and we shape our environment.

πŸ’¬ "I created an issue for it."

πŸ“’ "I did not bother to fix this thing today, where I have context, but hope a magic wizard will some day come and fix it."

πŸ’‘ Nobody is going to clean up after you. It is your code base, your responsibility. You are the one suffering from the mess you leave behind.

1️⃣ no-refactoring-issues - do not give yourself an easy excuse to offload work to your future self.

πŸ’¬ "I don't dare to touch this code path, because I don't know what breaks."

πŸ“’ "Our (automated) testing practice sucks. I don't trust it to meaningfully report whats broken, when I change things."

πŸ“’ "Our (automated) testing practice sucks. I know I have to fix 1000 tests after this small change."

πŸ’‘ A high functional test coverage gives you peace of mind that regardless of how much code you change things will be ok for your users.

2️⃣ architect-for-testability - build a system that is easy to test, and it will be well composed, easier to maintain, and understand. Support it with a comprehensive, fast, and reliable test suite.

πŸ’¬ "I don't understand what our users are doing."

❓ "How do you want to build a great experience supporting them?"

3️⃣ understand-the-business-domain - know what problems your users have, and choose the tools to support them wisely.

πŸ’¬ "I could not fix it."

πŸ“’ "I was too lazy to fix it, and I hope someone else will do it someday."

❓ Who is the magic wizard who can fix things for you?

4️⃣ easier-when-practiced - Yes, refactoring can be complicated. It only gets harder if we don't practice it, or let the code base erode further. Start refactoring today.

πŸ’¬ "I did not know cleaner was my job description."

πŸ“’ "I have a very simplistic view of what a developer's job is."

❓ Who on your team ensures things are maintainable for the long run?

5️⃣ embody-the-maintainer - ensure that the maintainer perspective is part of everything you do. Add it to your definition of done, enforce it.

πŸ’¬ "I am behind schedule to wrap up this issue".

πŸ“’ "I keep the code base in this state so I'll be behind schedule next time, too".

6️⃣ invest-in-tomorrow - do not accept today's excuses, invest reasonably to set yourself up for (your) future success.

Always leave the campground cleaner than you found it.

(Boy scouts of America)

When is a refactoring successful? Is it always worth it?

The way we approach things makes all the difference.

πŸ’¬ "Next time we should clean this up."

πŸ’‘ No body is going to prioritize your cleanup, it ships no user value. Clean it up now, in context, or keep suffering.

1️⃣ opportunistic-in-context - make reasonable efforts to clean up the mess you encounter, while you have context. It is cheaper now.

πŸ’¬ "I refactored, but the code base is no better now."

πŸ“’ "I changed things without a plan."

πŸ’‘ The feeling that "something is wrong" does not automatically lead to a successful outcome.

2️⃣ know-what-and-why - understand what you do and why, before you do. Use the plan to assess if the actual step of doing is feasible and worth the effort. If in doubt, get a second opinion.

πŸ’¬ "Refactoring makes a code review hard to execute."

πŸ“’ "I did not properly structure my work, so others can follow along."

3️⃣ make-it-explicit - separate refactoring and feature development. Document what changes, why. Structure work in multiple PRs, or multiple commits.

πŸ’¬ "Is this bike-shedding?"

πŸ’‘ Be concious about your time. Put your attention to places where it matters. Be able to argue what changes, why. Get buy-in from your team.

4️⃣ improve-substantially - cosmetic changes are a waste of time and attention - refactor where it is worth the effort. Manage technical debt wisely, you'll never fully get rid of it anyway.

πŸ’¬ "I refactored our test suite."

πŸ“’ "Now there is five ways to write our E2E tests."

Fortunately, the charging one has been solved now that we've all standardized on mini-USB. Or is it micro-USB? Shit. - XKCD/927

πŸ’‘ Every additional pattern you introduce puts a mental toll on your future self.

5️⃣ finish-what-you-started - ensure new patterns are recognizable and fully adopted, at the very least on the the happy path of your application. Unbuild old cruft.

Make refactoring and continuous improvement integral parts of our work.

Getting there is a cultural and technical challenge

Cultural, because we need to get over old habbits, change our way of working.

Technical, because the first steps to a better code base are the hardest, and skills have to be learned.

A healthy environment and following basic rules of refactoring can help.

Environment

Rules of refactoring

Note to managers

Thanks

❀️

Appendix

Things that may not work (1)

Things that may not work (2)

Simple reminders can go a long way

definition of done

Refactoring and (re-) architecting are related activities