All writing

Engineering

Technical Debt When You Also Run the Servers

Most debt advice assumes someone else maintains the code. Here is what changes when the person taking the shortcut is the one it wakes up.

Sagar Pandey · Founder13 min read

I write the code and I hold the pager. Those two facts control technical debt at this studio better than any process I could put on top of them, and they also make most of what is written about technical debt useless to me.

The standard article assumes a VP with capacity to allocate, a roadmap with a slot in it, and a team where the person who takes the shortcut and the person who maintains it are different people. We are a small studio in Daejeon with three products live in Korea, running on machines we administer ourselves. There is no twenty percent of engineering capacity here. There are evenings, and there is a client with a launch date.

So the question I care about is not how bad a piece of code is. Every codebase has bad code in it and most of that bad code is harmless. The question is what a particular shortcut charges, who it charges, and when the bill arrives.

Three debts, and only one of them compounds

Sorting debt by code quality tells you almost nothing. Sorting it by blast radius tells you what to do on a Tuesday afternoon.

Kind Where it lives Who pays, and when Carry it?
Local Inside one module, behind an interface that holds Me, only when I open the file Yes, sometimes forever
Foundational Data model, translation boundary, auth, money Everything built after it, on every feature No, fix it while it is still cheap
Operational Deploys, migrations, background jobs, logs Whoever is awake when production misbehaves No, this one bills with interest

Local debt is safe, and most debt is local

The moderation screens behind Keasy were built for us to use. They have duplicated markup, a component that takes eleven props, and at least one function I would be embarrassed to show anyone. They have also never cost me anything, because the cost of local debt is bounded by how often you open the file, and I open that file twice a year.

The test is short. If I never touch this code again, what does it cost? For seeding scripts, internal tooling, one-off exports and admin panels, the answer is usually nothing. Cleaning those up is a hobby, not work. I have shipped code I knew was ugly, on purpose, and never paid a cent of interest on it, and I would take that trade again tomorrow.

Copy and paste is in this category more often than people admit. Three copies of similar logic are a problem when the three are the same thing. When the three are genuinely diverging, and in multilingual products they usually are, the abstraction you write to unify them becomes worse debt than the duplication.

Foundational debt compounds because everything after it inherits the shape

The WS Phone Shop consulting app runs in seven languages. A hardcoded string in one component is a five minute fix. The decision to write screens with strings baked into them, before there is a translation boundary, is not forty five minute fixes. It is one wrong shape repeated forty times, and every screen built after it inherits the shape for free. That is the definition of compounding: the debt is not the code that exists, it is the code that has not been written yet.

The same thing happens one layer down, in the rows. Store a guide's category as free text and you have a field that works perfectly at thirty two guides. At three hundred, you have four spellings of the same category, no way to rename one without touching every row, and a filter that quietly drops content. Store it as a reference to a real category and renaming is one update. The two decisions look identical on the day you make them and cost differently forever.

The rule I use is that data outlives code. Screens get rewritten. Half of what any of our products launched with was redesigned once real users touched it. Rows do not get rewritten. Whatever a user typed, uploaded or paid for survives every redesign and every pivot, and so does the shape you chose for it. So take the shortcut in the layer you expect to throw away.

There is a practical reason to be strict here. The most expensive item that ever lands on my calendar is a migration that fixes a column on a live product: a backfill, a deploy window, a rollback path that has to be tested against real data, and a conversation with the client about why their app is degraded while it runs. Nothing about a bad column is expensive until the day it has to change, and then all of the cost arrives at once.

Operational debt bills at the worst possible hour

Our production runs on Postgres, Redis and Docker on infrastructure we operate. That means a whole category of debt that has nothing to do with how the code reads: the deploy step that exists only in my shell history, the migration with no down path, the background job that fails silently instead of retrying, the error branch that logs a message without the one id you would need to find the affected row.

This debt has the worst rate of the three, because it is always charged at the moment you are least able to pay it. You are a worse engineer at two in the morning. The code you write to stop the bleeding at that hour becomes tomorrow's local debt, and you will not remember writing it.

So I do not trade in this category, ever, no matter what the deadline is. Every deploy is one command and it lives in the repository, not in my history. Every migration is reversible, or it is declared irreversible in writing before it merges. Every background job either retries or gets loud. Every error path prints the identifiers you need to find the record. None of that is a quality standard. It is sleep insurance.

The person who takes the shortcut is the person who gets paged

In a large organization the incentive is broken by design. Shipping is visible, maintenance is not, and engineers change teams faster than debt matures, so the person who wrote the shortcut is usually somewhere else by the time it starts charging. Every process I have read about managing technical debt is a workaround for that one problem.

We do not have that problem. If I hardcode a carrier rule into a pricing screen in March to make a launch, I am the person opening that file in November when the subsidy tables change. That alignment is the strongest debt control I know of, and it is not a process, it is just the size of the company.

It has exactly one failure mode, and it is severe. I stop being able to see the debt, because I know all the workarounds. No codebase is hard to work in when you already have the map in your head. The debt stays invisible right up until the moment a second person opens the file, and by then it has been compounding for a year.

That makes onboarding the real audit. Whatever someone new gets stuck on in their first week is a more accurate debt inventory than anything a linter will tell me, and it has a short shelf life, because within a month they will have learned the workaround too. So it gets written down that week or it does not get written down at all.

The second test is operational and I apply it to every project before I call it finished. Could someone else deploy this on a Sunday without calling me? If the answer is no, the gap is debt, whatever the code quality looks like.

Write the note when you take the shortcut, not when it hurts

A TODO comment is a wish. It has no owner and no consequence, and after a year a codebase full of them is texture nobody reads.

What works instead is a note with a trigger. Not a date, because nobody has ever done a thing because a comment said the second quarter. A trigger is the condition that makes the shortcut wrong, written at the moment you can still see it clearly.

# DEBT(2024-11-04): the deposit line is formatted here and again in the
# poster renderer. Two copies of one rule.
# TRIGGER: the deposit rule changes, or a language needs a different
# currency format.
# COST IF UNPAID: every change to the quote layout gets made twice and
# the two outputs drift apart.

It is not a backlog and I do not review it on a schedule. The value is that when a client asks what it would cost to change how quotes are formatted, I can grep for DEBT in that area and find my own warning in ten seconds, before I give them a number. Most bad estimates I have given were not failures of judgement. They were cases where the relevant fact was in my head six months ago and not in front of me at the moment I was asked.

Telling a client the thing they want is blocked

This is the hardest part of the whole subject and it has nothing to do with engineering. Last quarter the client chose the cheaper option. This quarter they want the feature that the cheaper option made expensive. Four rules keep that conversation survivable.

Never put the word refactor on an invoice. Clients do not buy refactors and they are right not to. They buy the feature. The estimate for the feature includes the cost of the earlier shortcut, itemized in plain terms: adding the second carrier is five days, two of which are the carrier and three of which are moving the plan rules out of the screens, which we skipped in March to hit the launch.

The conversation is only survivable if the decision was written down at the time, in the client's language rather than the code's. One sentence in the delivery mail on launch day is worth more than any comment in the repository: we are hardcoding the plan rules for a single carrier to make this date, and adding a second carrier later will cost roughly a week. That sentence turns November from an accusation into a reminder.

Never say they declined to pay for it, even when that is precisely what happened. It was a shared decision. I recommended the shortcut, because the alternative was missing a launch, and missing the launch would have been worse for them than the debt. Owning my half of it is not politeness, it is accuracy.

Price the interest, not the sin. Give two options with numbers attached: pay it down inside this feature and the next three land at a normal price, or patch it again and the next one costs more than this one did. Clients make good decisions when the choice is framed that way. They make bad ones when the only options are yes and a lecture about code quality.

What a deadline is allowed to buy

Everything we have shipped went out as an MVP with a real date on it. The scope of an MVP is also the scope of the debt you are agreeing to, so the sorting happens before the launch, not after.

The SGP application suite is a clean example. It produces a filled resume as both a print layout and a downloadable PDF. If the print stylesheet is a mess internally, nobody suffers and I can carry that for years. But if the field list exists twice, once in the form and once in the PDF template, then the next time the university revises its official form we edit two places, and eventually one of them is wrong in a way that a student discovers at a submission desk. So the field definition gets to be one thing before launch, even under deadline pressure. The stylesheet does not get that protection.

Two things are never available to a deadline. Anything that decides who can see what, and anything that decides what somebody gets charged. A shortcut in either of those is not debt. It is a defect with a delay on it, and calling it debt is how it gets shipped.

Repayment in a studio with no refactor sprint

There is no reserved percentage here and pretending otherwise would just produce a number I quietly miss every month. Three mechanics do the actual work.

Debt gets paid inside the feature that trips over it. When a client asks for something that runs through a shortcut, the repayment is part of that estimate and gets done on that clock, because it is honestly part of the cost of what they asked for. Nothing gets cleaned up because it is messy. Something gets cleaned up because it is now in the way of paid work, which is a much easier standard to hold to at eleven at night.

Deletion is the cheapest repayment there is, and multilingual products make it cheaper still. A feature nobody uses costs review time, translation strings in every language you support, a slot in the navigation, and a constraint on every schema decision that comes after it. Removing one screen from a seven language product removes seven screens worth of maintenance. The best refactor is often a deletion, and it is the only one that makes the estimate for every future feature smaller.

The third mechanic is unglamorous and real: small studios have dead spans. Waiting on client feedback, on a store review, on a decision about a logo. That is when local debt whose trigger has already fired gets cleaned up. It is not scheduled, I have stopped pretending it is, and it accounts for more repaid debt than any plan I have ever written down.

The debt I have decided to keep

There are places in all three of our live products where I know exactly what is wrong and have chosen not to fix it. Search on the marketplace is a simple query that will need to become something better, and the trigger is inventory, not my taste. Some internal tooling is written for exactly one operator, and the trigger is the second operator. Quote formatting exists in more than one renderer, and the trigger is the next change to the rules underneath it.

Those are all fine, because I can name the condition that makes each one urgent. The debt that actually worries me is the other kind: the shortcut taken so long ago that it no longer looks like a shortcut, that everything since has been built on top of, and that I have stopped seeing entirely. That is what the notes are for, and it is why the first week of every new person on a project is worth writing down.

Topics

  • Technical Debt
  • Engineering Practice
  • Small Teams
  • Operations

Share this

Sagar Pandey

Sagar Pandey

Founder

Leads Montem Flumen and builds its web and mobile products hands-on.

Read the full profile

Reading about it is not the same as shipping it.

If something here describes a problem you are living with, tell us about it. We will say what it would take to fix, roughly what it would cost, and whether we are the right people for it.