Software as a Depreciating Asset
A few months ago, I used to think writing more code means better code. Boy, was I wrong. Working on a team of more experienced engineers than myself has taught me that the less you write, the better - most of the time. And I don't mean that fewer lines of code automatically means better software. Sometimes more code is exactly what you need. What I'm talking about is unnecessary code — code that adds complexity without adding enough value to justify the complexity it creates. Let me explain!
My first few months at Axle, most of the review comments on my PR would say things like “we already have a function that does this somewhere in the shared libraries, you don’t need to rewrite it, just use the one we have…”. And i’d be sitting there thinking - wait… where? 😂
I would then have to start searching through the codebase (thanks to AI for saving me a lot of time on this 😊). Eventually, I’d find the function. And sometimes I’d compare it to what I’d written and realize that the existing implementation wasn’t just reusable — it was actually better than mine. And, in many cases, it was significantly smaller. Isn’t that sorcery? 😂
Whenever a PR is merged, I notice that I have to spend some time understanding the code from that PR, how it fits into what we already have, and the complexities that come with it. The time it takes for me to fully understand these things depends on how the code was written, what problem it’s solving and how thorough and detailed the documentation is. I think you see where i’m going with this.
In the real world, unnecessary code usually means more surface area to understand, test, maintain, secure, deploy, monitor, and eventually replace. Yes, replace. As technology advances, code that worked perfectly six months ago may no longer work as expected — not because the code itself wore out, but because the environment in which it runs has evolved. And the code has to evolve with it.
Imagine writing:
if (hasPaid) {
letIn();
};Right now, this is basically free but in six months time, someone would have to know:
- Why does
hasPaidexist? - Where is it set?
- What happens if it's undefined or null?
- Which customers depend on it?
- Is this behavior documented?
- Does changing it break billing?
- Is there a test?
- Does another service depend on it?
- Is this still necessary?
- Who owns it?
That one little condition has created operational and organizational debt that must be paid before moving forward.
When I say software is a depreciating asset, I'm not talking about accounting. I'm using it as an engineering metaphor — the real depreciation is complexity. You can think about it roughly like:
Operational burden ≈ code × dependencies × interactions × change frequency
It's not a literal engineering equation, but it's a useful mental model. The important variable isn't merely lines of code but how many things can be affected when something changes. This is why technical debt is such a powerful concept.
Technical debt isn’t necessarily bad code. It’s broader than that. Technical debt is essentially a decision you make today that creates additional costs for future changes. One of the things I’ve noticed in working with different levels of engineers (hey, i’m not an expert 😂) is, initially, someone would write about 1500 lines of code and be happy and then a stronger engineer would solve that same problem with around 300 lines of code. Guess what? My CTO or an even stronger engineer would ask “Do we even need to write this code at all?” That question doesn’t mean he/she isn’t trying to solve the problem. Sometimes, an existing library already solved it, an AWS primitive solved it, a managed service solved it, the requirement isn’t actually necessary, a feature should be removed, the architecture can be simplified, etc. This is one reason experienced engineers can sometimes produce less code while delivering more value.
Code has “carrying costs”
Think of every piece of production code as something you now have to carry. At first, the cost feels almost zero. Then there's the reviewer, who spends a little more time understanding your code — the cost has already started. Once the code is merged, everyone else who eventually needs to work with it has to understand it too. More time. More cost. The rate at which the cost grows depends on how the code was written.
Code has an “option value”
This is where we deliberately accept some complexity today because it can make tomorrow’s changes cheaper. You can hardcode everything but there is something called abstraction, where we build infrastructure that lets the organization move faster later. Now you have optionality.
Abstractions can also become liabilities if not well managed. This is a trap many engineers fall into. They hear “avoid duplication” so they build abstraction for every single thing. Now nobody understands the system and the abstraction itself has become debt. This leads to an extremely important principle - An abstraction should pay rent. If an abstraction makes the future changes easier than the complexity it introduces, keep it. If it doesn’t, delete it.
Code deletion can be a form of engineering (this is something i’m trying to internalize and improve on). Imagine there were 12 services, 47 dependencies, 3 queues, 2 databases, 150k LOC and there’s one engineer that discovers that four services can be replaced by one simpler service, we can then have 8 services 29 dependencies, 2 queues, 2 databases, 110k LOC. The company didn’t lose 40,000 lines of value. It potentially removed 40,000 lines of liability. This is a completely different way of thinking about software.
You may have come across this principle - The best code is code you don’t have to write. Not because coding is bad but because software creates obligations. When you write code, you’re saying “someone will probably have to understand this later” and that someone might be me next month, another engineer next year or an engineer who wasn’t even at the company when you wrote it. This is why simplicity is such a powerful engineering advantage.
The goal isn’t less code. It’s less unnecessary obligation. I don’t think good engineering is about obsessing over line counts. A 50-line piece of clever code isn’t automatically better than 200 lines of boring, explicit code. Sometimes, the 200 lines are much easier to understand. Sometimes adding a layer of abstraction is absolutely worth it. Sometimes building a new service is the right decision. Sometimes technical debt is a completely rational tradeoff. The point is not whether to write less code but to be intentional about the obligations the code you write creates.
Every service, dependency, abstraction, database, and queue has a carrying cost. Every line of production code is potentially another thing someone will have to understand, maintain, secure, monitor, migrate, or eventually delete.
So before asking “How do I build this?”, maybe we should sometimes ask “Do we need to build this?”. And if we do “What's the simplest thing that solves the problem?” And finally “Will what I'm building make the next change cheaper or more expensive?”
I'm still learning to think this way myself but I've started realizing that becoming a better engineer isn't necessarily about learning how to write more code. Sometimes it's learning how to write less, delete more, simplify where possible, and make better decisions about what deserves to exist in the first place.