Every company I've worked with has one system like this. For one client, it was an invoicing system written in VB.NET sometime in the early 2010s, handling tax calculations, discount rules, and line-item totals for every invoice the company sent. It worked. It had worked for years. And when a new provincial tax rule came into effect and the finance team needed one number changed, the development team spent nearly three weeks too afraid to touch it.

Not because the fix was hard. The actual change was a handful of lines. The fear was about everything around those lines: nobody currently on the team had written the original system, there was no documentation describing what it was supposed to do, and there were no tests to catch it if the "small" fix broke a discount rule that had been quietly working since 2013. Everyone could see the code. Nobody could prove, with any confidence, what would happen if they changed it.

That fear is common, and it's completely rational. It's also solvable, and the fix isn't "migrate it" or "rewrite it in C#." Those decisions come later, if at all. The fix comes first, and it's smaller than most teams expect.

Why the fear is rational

A system becomes frightening to touch when three things are true at once: nobody fully understands what it does, there's no written record of what it's supposed to do, and there's no safety net to catch a mistake before a customer does. VB.NET systems from that era tend to collect all three, not because VB.NET itself is a bad language — it's a perfectly capable one that Microsoft still supports — but because systems written in that period, in that style, rarely came with automated tests, and the one or two developers who understood the business rules by heart have often moved on, moved up, or moved away from that part of the codebase entirely.

Add it up, and every change becomes a judgment call made on memory and hope instead of evidence. That's not a character flaw in the team. It's a completely reasonable response to working on something nobody can verify.

What "safe to touch again" actually means

Making a system safe to touch doesn't mean modernizing it, and it doesn't mean understanding every line of it before you're allowed to change anything. It means two concrete things, done in order, before the next change goes in:

First, write tests around what the system currently does — not what anyone assumes it does. This sounds backwards if you're used to testing new code, but for an existing system, the goal isn't to test an ideal design. It's to capture its actual, current behavior: given this invoice, with these line items and this tax rule, the system produces this total, today. Those tests aren't a judgment on whether the logic is good. They're a tripwire. If a future change produces a different number, something broke, and you'll know before a customer's invoice does.

Second, document what the system actually does, separately from what people assume it does. In almost every legacy system I've audited, there's a gap between the two — a discount rule that technically still fires under a condition nobody remembers setting up, a tax calculation that rounds differently than the spec says it should because of a fix made years ago and never written down. Writing down the real behavior, even briefly, turns "I think it does X" into "it does X, confirmed on this date, here's the test that proves it."

A concrete way to start

For the invoicing system, we didn't start by reading the whole codebase end to end — that's a multi-week task on its own and it delays the actual goal. We started with the invoices that mattered most: the highest-volume tax scenario, the most commonly applied discount, the handful of edge cases the finance team already knew were tricky. For each one, we wrote a test that fed in a known invoice and asserted the known correct total. That gave us maybe fifteen tests in a few days, covering the scenarios most likely to break and most expensive to get wrong — not full coverage, but real protection exactly where the fear was concentrated.

With those in place, the three-week-old tax fix took an afternoon. Not because the code got simpler, but because the team finally had a way to know, immediately, whether the change had broken anything that mattered.

Why this comes before any decision to migrate or replace

It's tempting to treat "it's scary to change" as a reason to rewrite the whole thing in a newer language. Sometimes that's the right call eventually. But deciding to migrate or replace a system you don't yet understand, with no tests describing its real behavior, just moves the same risk into a bigger, more expensive project. You'd be rebuilding guesses, not requirements.

Tests and documentation of actual behavior do double duty here: they make the existing system safe to maintain right now, and if a migration does happen later, they become the specification for what the new system has to do. Either way, the investment isn't wasted, and either way, it has to happen before a bigger decision, not after.

The system was never really the problem

Nobody on that finance team was wrong to be careful with code they didn't fully understand and couldn't verify. That caution was doing its job. What changed wasn't the VB.NET system itself — it still runs the same logic it always has. What changed was that the team could finally see what it was doing, prove it, and change it without holding their breath.

That's what "safe to touch again" looks like in practice: not a rewrite, not a leap of faith, just evidence where there used to be only memory.

Let's talk through your situation.