The Code You Write Today Is Someone's Problem Tomorrow

The Code You Write Today Is Someone’s Problem Tomorrow

My author bio ends with a sentence I’ve been carrying for years:

The code you create is a valuable legacy, so it’s important to build it carefully.

It sounds like something you’d frame and hang above a whiteboard. It isn’t. It’s the distilled result of watching systems survive their authors, outlive their requirements, and eventually become someone else’s problem — sometimes that someone else being me, years later, at 2 AM.

This article is the story behind that sentence.

What “Legacy” Actually Means

The word legacy in software has been colonized by negativity. “Legacy system” means old, unmaintainable, the thing you inherited and wish you hadn’t. People say it like an apology.

That’s not how I use it.

A legacy is what you leave behind. It can be a gift or a burden — and the difference is almost entirely determined by how carefully it was built. The Colosseum is a legacy. So is every static readonly Dictionary<string, object> that someone thread-unsafe-cached against a singleton in 2014 and then shipped to production without tests.

Both will outlast their creators. Only one will be admired.

The Compounding Cost of Carelessness

In nearly twenty years of .NET systems, the most expensive decisions I’ve witnessed weren’t made by incompetent people. They were made by skilled engineers in a hurry, under pressure, with incomplete context, who told themselves: “We’ll clean this up later.”

Later never comes. Or rather, it comes in the form of an incident.

Consider what “building carefully” actually costs at the moment of creation:

  • Enabling nullable reference types in a new project: minutes
  • Enabling them three years later across 200,000 lines: months
  • Adding an .editorconfig with analyzer rules at project start: one afternoon
  • Enforcing consistency across an organic codebase after four teams touched it: a quarter
  • Writing a proper CancellationToken propagation pattern from the start: trivial
  • Retrofitting cancellation into an async call tree that never anticipated it: surgical, risky, and slow

The CancellationToken case is worth pausing on, because it’s so easy to defer and so expensive when you do. A call tree without cancellation looks harmless:

public async Task<Report> GenerateReportAsync(int customerId)
{
    var orders = await _orderRepo.GetOrdersAsync(customerId);
    var invoices = await _invoiceRepo.GetInvoicesAsync(customerId);
    var pdf = await _pdfService.RenderAsync(orders, invoices);
    return new Report(pdf);
}

A year later, HTTP timeouts fire while the PDF renderer keeps allocating and the database queries keep running — because there’s nothing to stop them. Retrofitting cancellation now means touching every signature in the chain, every interface, every test, every caller. Versus what “careful at creation time” looked like:

public async Task<Report> GenerateReportAsync(int customerId, CancellationToken ct = default)
{
    var orders = await _orderRepo.GetOrdersAsync(customerId, ct);
    var invoices = await _invoiceRepo.GetInvoicesAsync(customerId, ct);
    var pdf = await _pdfService.RenderAsync(orders, invoices, ct);
    return new Report(pdf);
}

One parameter. Thirty seconds. That’s the decision that was “not needed yet.”

This is not a coincidence. It is compounding interest on technical debt, and the interest rate is not linear. The further the decision recedes into the past, the more the code has grown around it, the harder it is to reach, and the more things break when you try.

Careful building is cheap. Careless building is cheap too — until it isn’t. And it always stops being cheap at the worst possible moment.

What “Carefully” Does Not Mean

I’ve made a mistake I see others repeat: confusing “carefully” with “perfectly.”

Perfectly is a trap. It produces over-engineered systems that look impeccable in architecture diagrams and are misery to extend. I have taken over projects from consultants who preached Clean Code and delivered something that could not change without collapsing. Everything was carefully named, carefully layered, carefully documented — and completely rigid.

That’s not careful. That’s fearful.

Careful means four things — none of them perfectionism.

Understanding the operating costs of what you write. A Dictionary is not thread-safe. An async void swallows exceptions silently. A Guid.NewGuid() primary key fragments your index with every insert. Not obscure knowledge — basic operating costs that change the failure mode of code that otherwise compiles and ships fine. async void is the instructive one: exceptions escape unobserved, hit the thread pool, and crash the process with no stack trace pointing back to the source:

// async void: exception becomes unobservable noise
private async void OnMessageReceived(object sender, MessageEventArgs e)
    => await ProcessMessageAsync(e.Message);

// async Task: caller can catch, log, and handle
private async Task OnMessageReceivedAsync(object sender, MessageEventArgs e)
    => await ProcessMessageAsync(e.Message);

The Guid case is slower-burning. Both versions below ship on day one. The difference shows up in production monitoring three months later, when you notice your index is 60% fragmented and inserts are taking four times longer than they should:

public Guid Id { get; set; } = Guid.NewGuid();        // random, causes page splits
public Guid Id { get; set; } = Guid.CreateVersion7(); // monotonically increasing, .NET 9+ (see: https://learn.microsoft.com/en-us/dotnet/api/system.guid.createversion7)

Optimizing for the reader, not the writer. The next person to read this code is often you, six months from now, with no memory of what you were thinking. Deliberate code — code that makes its assumptions visible — is not slower to write. It’s more expensive to start and cheaper to maintain forever after.

Knowing when good enough actually is good enough. Careful is not exhaustive. Configuration loaded once at startup does not need nanosecond optimization. A nightly batch job does not need payment-processor reliability. Misapplied care creates its own form of debt — rigidity dressed up as quality.

Making the implicit explicit. The most dangerous code in any system isn’t complex code — it’s code where critical assumptions live in someone’s head instead of in the type system or the tests. The two implementations below are functionally equivalent on a happy path. Only one survives a new developer joining the team:

// assumptions in the developer's head
public class InvoiceService
{
    private Dictionary<int, decimal> _taxRates;
    public string FormatAmount(decimal amount, int regionId) =>
        $"{amount * _taxRates[regionId]:C}";
}

// assumptions in the compiler
public sealed class InvoiceService
{
    private readonly IReadOnlyDictionary<int, decimal> _taxRates;

    public InvoiceService(IReadOnlyDictionary<int, decimal> taxRates)
        => _taxRates = taxRates ?? throw new ArgumentNullException(nameof(taxRates));

    public string FormatAmount(decimal amount, int regionId)
    {
        if (!_taxRates.TryGetValue(regionId, out var rate))
            throw new KeyNotFoundException($"No tax rate configured for region {regionId}.");

        return $"{amount * rate:C}";
    }
}

The second version is longer because it encodes what was previously undocumented: tax rates are required, null is not acceptable, and an unknown region is a programming error — not a silent zero that produces a wrong invoice.

Code Outlives Context

Here is the thing that took me the longest to internalize:

The context in which you wrote the code will not survive. The business requirement that made the trade-off obvious will be forgotten. The pressure that justified the shortcut will be invisible. The Slack thread explaining why the timeout is hardcoded to 30 seconds will scroll into history. The team that understood the design will disperse.

What remains is the code.

And someone will have to work with it without your context, your justifications, or your intentions. They will read what you wrote and form conclusions. They will extend it, debug it, and curse it — or understand it and be grateful.

That is the legacy.

I have been both recipients. I’ve inherited systems where everything was explained by what the code did — where reading a class told you not just how it worked but why, what it was protecting against, and where the landmines were. I’ve also inherited systems that required six months of archaeology before I trusted any change I made.

The engineers who wrote both kinds were equally intelligent. The difference was care.

The Relationship Between Care and Speed

Teams that haven’t experienced this tension believe that careful code is slower to produce than careless code. They’re right in the short term. A quick hack ships faster than a considered design — once.

What they miss is the asymmetry in the other direction.

Careless code is expensive to extend, expensive to debug, expensive to test, expensive to hand off, and expensive to explain. Every future interaction with that code costs more than it needed to. The total cost of ownership grows with the number of future interactions, and production code has a lot of future interactions.

Careful code costs more upfront and less every time after.

This is not an abstract economic argument. I can point to specific decisions in systems I maintain where five minutes of thinking at creation time would have saved months of debugging over the lifetime of the feature. I can also point to the opposite: careful designs that held up under four years of changing requirements without needing to be rewritten.

The careful code was not slower to develop. It was slower to start and faster to finish — across the entire lifecycle of the feature.

What I Actually Do Differently

After nearly twenty years, “build carefully” has specific practices attached to it. These are not aspirational principles. They are the concrete things I do, or insist my teams do, because I’ve felt the cost of not doing them.

Enable Roslyn analyzers from day zero. Not as a code review substitute — as a safety net that operates at compilation time. I configure them in .editorconfig at project creation, severity-as-error for the things that matter, and when they produce noise I fix the noise rather than silence the rule. The six rules I never start a project without:

[*.cs]
dotnet_diagnostic.CA2007.severity = error   # ConfigureAwait missing
dotnet_diagnostic.CA1031.severity = warning # catch Exception (too broad)
dotnet_diagnostic.CA1051.severity = error   # public instance fields
dotnet_diagnostic.CA1825.severity = error   # unnecessary array allocation
dotnet_diagnostic.CS8600.severity = error   # nullable dereference
dotnet_diagnostic.CS8602.severity = error   # possible null reference

These rules catch bugs that appear in incident reports, not in code review — which is exactly the point.

Write the summary before the method. Not a docstring — a sentence in my head: “This method does X and assumes Y.” If I can’t complete that sentence clearly, I don’t understand my own code well enough to ship it. This sounds trivial. It isn’t. It catches underspecified designs before they become permanent.

Treat TODO comments as deferred decisions, not reminders. Every // TODO: fix this properly is a piece of context that will expire. Either I fix it now, create a tracked issue with enough context that a stranger could complete it, or I accept that it will never be fixed and stop pretending otherwise. The lie that “we’ll come back to this” is one of the most expensive fictions in software.

Read the diff before every commit. Not to catch typos — to notice surprises. If I see code I don’t remember writing or can’t explain, that’s the signal. Familiar code that suddenly looks strange is often code that shouldn’t be committed yet.

Name things for what they are, not what they do. CustomerRepository tells you the mechanism. CustomerAccess is vague. ActiveCustomersByRegionQuery tells you what you’re getting and why. The noun matters. The qualifier matters more.

The Longer Arc

I carry that motto in my bio because it is the most honest thing I can say about why I write the way I write and build the way I build.

It isn’t about perfectionism. It isn’t about impressing code reviewers or following the fashionable methodology of the moment. It’s about the relationship between present decisions and future consequences — and taking that relationship seriously enough to slow down slightly, every single time, and ask: “Is this how I would want to find this?”

Most of the time, the answer is no. That’s fine. That’s the question working.

The code you write today will be maintained by someone who doesn’t know what you were thinking. It might be a colleague. It might be a future version of yourself. It might be someone you’ll never meet, building on a library you published and forgot about.

Do them the courtesy of building it carefully.

Comments

VG Wort