Code Ratcheting

·5 min read·Graham Mace
Code Ratcheting

Code Ratcheting: A Practical Guide to Safe Refactoring

“The key to effective refactoring is recognising that you go faster when you take tiny steps, the code is never broken, and you can compose those small steps into substantial changes.” – Martin Fowler, Refactoring: Improving the Design of Existing Code

When supporting legacy systems while trying to modernise the codebase, the goal becomes clear - not change what the code does, but only change how it does it. This requires refactoring done carefully, incrementally, and with confidence that nothing breaks along the way.

The Problem: URL Routing as a Blocker

In ASP.NET MVC, there are two approaches to populating the URL routing table for controllers:

With conventional routing, the route table is populated based on controller names and their location in the project. This works fine until you need to reorganise things-moving controllers around automatically breaks URLs because the routing is implicitly tied to physical structure.

Fortunately, configuration over convention can help. By adding RoutePrefixAttribute and RouteAttribute to every controller, you can explicitly specify the URL each controller should expose. Then register these using:

routes.MapMvcAttributeRoutes();

However, this is a huge, risky change to undertake. Having automated test suites (like Postman collections) as a safety net makes this kind of structural migration far less nerve-wracking.

What Is Code Ratcheting?

With fast feedback cycles in place, we can move beyond theoretical designs and begin improving the code itself. The approach-known as Code Ratcheting-works as follows:

  1. Configure the build to fail if the number of violations increases beyond a set threshold (no new violations allowed)
  2. Lower the threshold in tandem as the total number of violations decreases
  3. Fail the build if new code introduces violations that exceed the current threshold
  4. Merge safely when the build passes, confirming no new violations were introduced

This approach enables the codebase to scale by reducing inconsistent code while preventing regression back into a Big Ball of Mud.

Of course, one Pull Request might reduce violations while another introduces as many, keeping the total under the threshold. In those cases, automation alone cannot be relied upon-at least until the build failure threshold reaches zero.

To achieve this goal, you can leverage static analysis tools by incrementally fixing violations namespace-by-namespace until the threshold is eventually dropped to 0.

The Execution: Moving Code Around

Provided behaviour doesn’t change, moving code around in a compiled language such as C# is relatively straightforward. If the code compiles after moving types, it’s fairly safe to assume the functionality remains intact.

Small changes at a time, with at least two pairs of eyes reviewing what’s being changed, is essential.

A Typical Refactoring Sequence

Here’s what a realistic refactoring workflow looks like:

TaskDescription
Set baselineConfigure StyleCop/R# inspection threshold (e.g., 4269 violations)
Fix by namespaceAddress violations incrementally in Constants, Configuration, Exceptions, etc.
Adjust severityDowngrade certain rules from warnings to hints where appropriate
Clean upRemove unused references, empty folders, and obsolete files
Lower thresholdGradually reduce violation limits (4269 → 153 → 76 → … → 0)

Feature Flagging as a Safety Mechanism

Feature Toggles provide a way to dynamically change code at runtime without needing to redeploy-they’re essentially an ‘if’ statement around a block of code.

When organisations start with homegrown feature flagging solutions, one valuable feature is often missing: a UI for updating flag states. To change a flag value, engineers typically need to open Pull Requests, wait for review, merging, building, and deployment. This lengthy process can become a bottleneck, especially during production incidents.

Modern feature flag platforms solve this by allowing flag states to be changed via a UI without requiring redeployments. Teams can have their own logins, and non-engineering folk can update flags if needed.

Avoiding Anti-Patterns

There’s a dangerous anti-pattern worth noting: some teams allow the application itself to update feature flags via API keys embedded in production deployments. This happens when seat licensing is insufficient and teams “work around” procurement delays by patching over organisational process deficiencies with technical solutions.

Used correctly, feature flags are pivotal to any refactoring effort. Being able to dynamically change code paths at runtime without redeployment is incredibly powerful:

  • Bad release? Simply turn off the feature flag
  • Fix the issue, re-release, turn the flag on again

Having feature flagging functionality in a centralised project that can be referenced anywhere in the solution is crucial for any refactoring efforts.

Refactoring Legacy Code: A Step-by-Step Approach

Once you’ve established the infrastructure, you can tackle the actual refactoring. But where do you start?

Counting raw lines of code can give some indication of which codebase areas are the largest-and therefore likely most complex. Assigning priority based on size helps make informed, data-driven decisions about which namespaces to refactor first.

For each namespace, the recommended approach is:

  1. Leave existing legacy code as-is and use it as a reference
  2. Rewrite a new implementation behind a feature flag
  3. Write unit tests over the new implementation
  4. Update or create any integration tests required
  5. Test thoroughly to ensure the endpoint works exactly as before
  6. Merge to master and release to production
  7. Add monitoring alerts targeting the specific endpoint
  8. Turn on the feature flag and monitor closely
  9. Remove the legacy code behind the feature flag and remove the flag itself
  10. Archive the feature flag from your flag management system

Does All Code Need Refactoring?

It’s clear there’s considerable work involved in refactoring legacy code. But the more fundamental question remains: does some code need to be refactored at all?

Sometimes the answer is yes-when technical debt is actively blocking progress, causing bugs, or making maintenance prohibitively expensive. Sometimes the answer is no-when code works, is understood, and changing it introduces unnecessary risk.

The art lies in knowing the difference.


This post reflects techniques and practices developed during work on large-scale API modernisation projects. The principles apply regardless of company or technology stack.

Want More?