# Code Ratcheting

*2026-09-20*

> A practical guide to safe refactoring


# 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*](https://www.amazon.com.au/Refactoring-Improving-Design-Existing-Code/dp/0201485672)

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](https://dotnet.microsoft.com/en-us/apps/aspnet/mvc), there are two approaches to populating the URL routing table for controllers:

- **[Conventional Routing](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-10.0#conventional-routing)**
- **[Attribute Routing](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/routing?view=aspnetcore-10.0#attribute-routing-for-rest-apis)**

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:

```csharp
routes.MapMvcAttributeRoutes();
```

However, this is a huge, risky change to undertake. Having automated test suites (like [Postman](https://www.postman.com/) 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](https://en.wikipedia.org/wiki/Spaghetti_code#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:

| Task             | Description                                                                                                                                                                      |
|------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Set baseline     | Configure [StyleCop]({{< relref "/posts/2024/copping-some-style" >}})/[R# inspection]({{< relref "posts/2024/re-sharpening-your-codebase" >}}) threshold (e.g., 4269 violations) |
| Fix by namespace | Address violations incrementally in Constants, Configuration, Exceptions, etc.                                                                                                   |
| Adjust severity  | Downgrade certain rules from warnings to hints where appropriate                                                                                                                 |
| Clean up         | Remove unused references, empty folders, and obsolete files                                                                                                                      |
| Lower threshold  | Gradually 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](https://launchdarkly.com/) 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](https://en.wikipedia.org/wiki/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?

- Check out Martin Fowler's [Refactoring](https://refactoring.com/) book
- Explore [LaunchDarkly](https://launchdarkly.com/) for feature flagging
- Read about [Code Smells](https://martinfowler.com/bliki/CodeSmell.html) and when to address them
