What Being a Tech Lead Taught Me About Writing Better Code
by Arif Ikhsanudin, Backend Developer
The fastest way to understand what makes code good or bad is to become responsible for code you didn't write. Tech leading changed how I write permanently.
Suddenly Other People's Code Was My Problem
The transition from individual contributor to tech lead comes with a particular kind of discomfort: you are now responsible for the quality of code you didn't write, don't fully understand, and can't always find time to fix. You also spend enough time in other people's code that your opinions about what makes code good get tested against reality at scale.
What I thought "clean code" meant before: well-formatted, reasonably commented, following the style guide.
What I understood it to mean after six months of tech leading: code that a tired developer can safely modify at 11 PM under pressure without causing a new incident.
That framing changed everything about how I write.
The Reader Is Not You
When you write code alone, you're also its main reader. You know what you meant. You know which edge cases you considered and which you punted on. The names you chose make sense because you were there when you chose them.
When you become responsible for a codebase written by a team, you experience what it's like to be a reader who wasn't there. Suddenly the things you used to do in your own code — the slightly opaque variable name, the function that does two things, the comment that says "fix later" — become friction you feel viscerally.
Write for the person who picks this up at 2 AM. That person is frantic, undertired, and trying to find exactly one thing as fast as possible. Are they going to understand what this function does from its name? Are they going to find the edge case handler, or are they going to miss it and cause an outage?
Code review from a tech lead perspective forced me to ask those questions about my own code, not just other people's.
Small Functions Aren't About Style
I used to think the preference for small, single-purpose functions was partly aesthetic — it was the "proper" way to write code, recommended in books, favored by linters. Something you did to be a good citizen.
As a tech lead reviewing PRs and onboarding new engineers, I understood it differently. Small functions are navigational aids. When every function does one thing and is named for that thing, a new developer can find their way through an unfamiliar codebase by reading names. They don't have to open every function body to understand what's happening at the call site.
Large, multipurpose functions invert this. Every call site is a black box. Understanding what changes where requires reading bodies, not names. The cognitive cost is proportional to team size.
I now write small functions not because I was told to, but because I've watched what happens when you don't.
Error Handling Is Architecture
As an individual contributor, I handled errors where it was convenient. Try-catch at the point of failure, generic exception rethrown up the stack, maybe a log line.
As a tech lead dealing with production incidents, I started to see error handling as a design concern, not an implementation detail. The questions that matter:
- Where in the call chain does an error belong?
- What information does the caller need to handle it appropriately?
- What information does the operator need to diagnose it?
- What's the failure mode — does the system fail loudly or silently?
Silent failures are the worst. A system that swallows errors and returns bad data is harder to debug than one that crashes dramatically. At least a crash produces a log.
I now think about error propagation the same way I think about data flow. It's a first-class design decision with architectural implications, not something to figure out after the happy path is working.
The Test Is a Specification
Before tech leading, I wrote tests to verify behavior. After, I started to see them differently: a test is a specification of what the code is supposed to do, written in a form the next engineer can run.
When I onboarded someone to a service I'd built, the tests were their map. If the tests were shallow or sparse, the new engineer had no way to understand what guarantees the system made. If the tests were comprehensive and well-named, they could understand the service's contract without reading all the source.
A well-named test is documentation that doesn't go stale. returns_empty_list_when_no_matching_records_found() tells the reader something concrete and checkable. test_query() does not.
I now write tests before or alongside code, and I name them as specifications: what does this system guarantee? Under what conditions? What are the edge cases it explicitly handles?
Simplicity Is a Team Property
The insight that changed my writing the most: simplicity isn't about you. It's about the team.
Code that's simple for an experienced developer on a Wednesday afternoon might not be simple for a junior developer on a Monday morning or anyone at 2 AM. The goal isn't code that's intellectually minimal — it's code that the weakest link in the team can safely modify without guidance.
That's a higher bar than it sounds. And meeting it consistently requires actively resisting cleverness, resisting abstraction for its own sake, and resisting the urge to optimize before you have evidence that optimization is needed.
The best compliment I've received on code: "This was easy to change."
You don't fully understand what your code costs until someone else has to maintain it — and by then, you can't take it back.