My Biggest Enemy Writes My Code

My Biggest Enemy Writes My Code

There’s an engineer I’ve worked with for nearly twenty years. He’s technically skilled, reasonably intelligent, often under pressure, and thoroughly convinced that Future Self will clean up whatever he leaves behind.

His name is Past Self. He’s my arch enemy. And he writes all my oldest code.

This is the second part of the Code as Legacy series. In part one, I made the case that code is a legacy (something you leave behind), and that the difference between a gift and a burden is almost entirely determined by how carefully it was built. This part is about what happens when you weren’t careful. About the person responsible. And about the uncomfortable realization that Past Self and Future Self are the same person, separated by time and context and the slow erosion of memory.

Past Self, Characterized

Past Self is not a villain. That’s the first thing to understand, and the most annoying one.

He was usually working under real constraints: a deadline that wasn’t negotiable, a requirement that kept changing, a codebase he inherited and didn’t fully understand. He made the trade-offs that made sense at the time, with the information he had. I know this because I was there. I remember the Jira ticket. I remember the conversation that ended with “just get it working for now.”

What Past Self lacked wasn’t intelligence or intent. He lacked two things: imagination and humility.

He couldn’t imagine that the code would still be running three years later in a context he’d never anticipated. And he wasn’t humble enough to admit, at the moment of the shortcut, that he was making a permanent decision while pretending it was temporary.

I know this because I still catch myself doing it.

The Evidence File

Every codebase I’ve worked on long enough has what I mentally call an evidence file: a collection of decisions Past Self made that Future Self is currently paying for. Here are a few entries from mine.

The connection string that became a foundation.

Early in a project, there was a SQL connection string in appsettings.json. Direct, clear, no abstraction. It worked. Nobody moved it when the project grew. Then it got referenced in six places. Then someone built a multi-tenancy feature that assumed a single database. Then we needed to support read replicas. By the time Future Self arrived at this problem, the connection string wasn’t a configuration value anymore. It was structural. Changing it meant touching half the service layer.

Past Self had forty seconds to introduce an abstraction. He didn’t, because “we’ll refactor when we need to.” Future Self needed two sprints.

The bool parameter that grew up.

// Past Self, six years ago
public async Task SendNotificationAsync(int userId, bool isUrgent)

Reasonable at the time. Two states, clear semantics. Then came “also high priority but not urgent,” then “urgent but silent,” then “urgent and high-priority and batched.” The method signature became:

// Future Self, inheriting the mess
public async Task SendNotificationAsync(
    int userId,
    bool isUrgent,
    bool isHighPriority,
    bool isSilent,
    bool isBatched)

Five booleans. All positional. All looking identical at every call site. All impossible to read without hovering over the method signature. Past Self’s bool was the reasonable starting point. The problem was that nobody stopped to redesign when it started multiplying:

// What Future Self eventually had to write anyway
public async Task SendNotificationAsync(int userId, NotificationOptions options)

public sealed record NotificationOptions(
    NotificationPriority Priority = NotificationPriority.Normal,
    bool Silent = false,
    bool Batched = false);

public enum NotificationPriority { Normal, High, Urgent }

This was always the right shape. Past Self just didn’t know it yet, and neither did I, when I was him.

The log statement that ate the disk.

_logger.LogInformation("Processing order {OrderId}: {@Order}", orderId, order);

{@Order} serializes the entire object. Including the Customer navigation property. Including the Customer.Orders collection. Including each of those orders’ Customer navigation properties. On a Tuesday morning with normal traffic: fine. On Black Friday, with order volume at 40× normal: the logging pipeline wrote 800 MB of JSON per minute, filled the disk, and took down the service.

Past Self was debugging something. He wanted to see the full order object. He committed the log line and forgot it was there.

Future Self found it during a post-mortem at 3 AM.

Why You Can’t Fire Past Self

The obvious response to all of this is: why didn’t you fix it at the time? Why didn’t you write it correctly from the start?

Sometimes the answer is genuine negligence, and I won’t pretend otherwise. But more often, Past Self was operating under a set of conditions that made the decision locally rational even if it was globally wrong:

He didn’t have the full picture. The connection string was in appsettings.json because nobody had decided on a multi-tenancy strategy yet. The bool was bool because the requirements only described two states. Decisions that look obviously wrong in retrospect were made before the retrospect existed.

He was optimizing for the wrong horizon. Software development has strong incentives to ship now and a much weaker feedback loop for the cost of what you shipped. Past Self felt the deadline. He did not feel the two-sprint refactor that happened three years after he’d moved on to a different feature.

He told himself it was temporary. This is the one I find hardest to forgive, because it’s the most deliberate self-deception. “We’ll clean this up” is a phrase Past Self used as a get-out-of-jail card, knowing full well who would be holding the bill.

That person is me. Future Self is not some abstract successor or a colleague who joins the team later. Future Self is me, roughly twelve months from now, with no memory of what I was thinking today, inheriting whatever I ship this week. He doesn’t get a briefing. He gets a diff.

You can’t fire Past Self because he’s already gone. All you can do is clean up after him, try not to become him, and (this is the part that matters) think carefully about what you’re about to hand yourself.

The Asymmetry Problem

Here’s what makes Past Self so dangerous: the cost of his decisions is borne entirely by Future Self.

This asymmetry is not unique to software. It shows up everywhere that consequences are deferred: environmental policy, infrastructure maintenance, pension systems. The person who makes the decision and the person who lives with it are not the same person. This creates a systematic bias toward decisions that look good now and cost later.

In software, the version of this that I see most often is what I’d call the invisible tax. Past Self doesn’t add a line item to the budget for his shortcuts. He doesn’t log the future cost anywhere. Future Self just finds, gradually, that everything is harder than it should be. Features take longer. Bugs are more frequent. Changes in one place break things in unexpected places. Nobody points at a specific decision and calls it out, because Past Self’s decisions are distributed across thousands of lines of code, each one small and deniable, each one contributing to a codebase that resists change at every turn.

The tax is real. It’s just invisible until you try to spend.

What Future Self Deserves

This is the part Past Self consistently gets wrong: Future Self isn’t an abstraction. He’s me, a year from now, with no memory of what I was thinking today. He inherits my shortcuts the same way I inherited Past Self’s, not as a debt somebody else took on, but as his problem to solve with whatever time and energy he has left after dealing with everything else.

He’ll find the code in the middle of something else. He’ll have thirty minutes to understand what I wrote and why, fix whatever broke, and get out without making it worse. He won’t have my context. He won’t have the Slack thread. He won’t have the meeting where I decided the timeout should be 30 seconds because the legacy service was slow and the client couldn’t wait for a proper fix.

What he deserves is code that doesn’t require archaeology to understand.

This doesn’t mean over-documentation. It doesn’t mean exhaustive comments. It means code that makes its assumptions visible, surfaces its constraints, and fails clearly when something goes wrong. It means the difference between:

// Past Self
var timeout = 30000;

and:

// Future Self can understand this without a Slack thread
// Matches the SLA of the legacy ReportService endpoint (see ADR-042)
private static readonly TimeSpan ReportGenerationTimeout = TimeSpan.FromSeconds(30);

One is a magic number with no explanation. The other is a decision with enough context that Future Self can evaluate whether the constraint still applies, and change it if it doesn’t.

The comment here is justified precisely because it encodes why, not what. The what is obvious. The why was in someone’s head, and now it isn’t.

The Uncomfortable Continuity

I’ve been writing about Past Self as if he’s a separate person. He isn’t.

Every piece of code I write today becomes part of Past Self’s legacy within the year. The shortcut I take this afternoon because the sprint ends on Friday will be Future Self’s archaeology project sometime in 2027. The // TODO: handle this properly I leave in because I’m tired becomes the thing that nobody ever comes back to fix.

The uncomfortable truth is that Past Self is not a character from my past. He’s a character I’m actively writing right now: every time I ship something I know isn’t quite right, every time I leave a decision implicit that should be explicit, every time I tell myself Future Self will deal with it.

He won’t deal with it. He’ll be too busy dealing with something else Past Self left behind.

Making Peace Without Excusing

I’ve made peace with Past Self, more or less. Not because he didn’t cause damage. He did, measurably, in sprints and in incident hours and in engineers who got frustrated and left. But because the alternative to making peace is a kind of paralysis that doesn’t help anyone.

What I haven’t done is excuse him.

Making peace means: I understand why you made those decisions. I understand the constraints, the pressure, the incomplete picture. I know you weren’t trying to create problems.

Not excusing means: you still should have known better on some of this. The magic numbers. The deferred decisions you knew were permanent. The // TODO comments you never intended to come back to. Those weren’t forced on you by constraints. Those were choices.

The difference matters because excusing everything Past Self did means never learning anything from him. And making peace means I can look at the evidence file without anger, figure out what’s worth fixing and what isn’t, and move forward.

What I’m Handing Future Self

Here’s where I get to confess: this article is partly an accountability document.

I maintain systems that have Past Self’s fingerprints all over them. Some of it I’ve fixed. Some of it I’ve accepted as the cost of the original decisions. Some of it I’m actively making worse right now, probably, in ways I can’t see yet.

What I’m trying to do differently (and what I’d argue is the only practical response to the Past Self problem) is to make the implicit explicit, every time, even when it’s inconvenient. Not to write more code, but to write code that explains itself. To make the assumptions visible, the constraints documented, the failure modes clear.

Future Self will still find things Past Self left behind. That’s inevitable. What I can control is whether Future Self finds them with enough context to understand what he’s looking at, or whether he has to figure it out from first principles at 3 AM while something is broken in production.

The code I write today is a letter to myself: to someone who will have no idea what I was thinking, who will be under pressure, who will need to understand this quickly and get out cleanly. I know who Future Self is. I know what his days look like, because they look like mine.

I’m trying to write him clearer letters.

This is part two of the Code as Legacy series. Part one covers what “building carefully” actually means in practice.

Comments

VG Wort