Code was the smallest part of the job
After more than twenty years in the industry, I now understand things are not like I imagined, nor what some people talked about. Most of what we hear or read is about coding, yet what I’ve found is that most of what we do in our careers isn’t.
Like most others, at the beginning of my career, my focus was to get faster at writing code, improve its structure, and learn programming languages in depth. It was worth it. But these days, all of these things can be done by AI on the same level or even better. The parts that actually compound, the parts that get you trusted with hard problems, are almost entirely somewhere else.
Although code is important, the software we build is what endures for years to come. Building such software is not easy. The 13 lessons below are what I figured out about doing the second thing well, after the first one stopped being the bottleneck.
In particular, we will discuss:
The job is mostly debugging. Most days, you’re not writing new code. You’re checking logs and trying to understand what is not working, and AI can’t do that part for you yet.
Code is a liability, not an asset. Every new line is a bill we need to pay. Treat the code as a cost, and we should avoid most over-engineering.
Solve the right problems, not just the assigned ones. Closing tickets is not the same as moving the system forward. The engineers who get noticed pick the problem nobody assigned, not just the ones already in the queue.
Plan before you code. One hour of thinking before the editor opens saves days of rewriting later.
Simple is hard to achieve. Most codebases get complex because adding is easier than removing. The best ones I’ve read look almost boring, and that’s the point.
Tests are a contract with future-you. The tests you skip turn into the bug that finds you on a weekend. Nobody adds them later. Nobody.
Bring solutions, not just opinions. Anyone can call a project bad. What’s rare is showing up with the next move, a one-page proposal, and something concrete enough to argue against.
Code wins arguments. Build the smallest version that runs, and the conversation moves from opinions to hard facts. With AI in the loop, building one has never been cheaper. There’s no reason to argue in the abstract anymore.
Engineering is more about people than tech. Nothing in tech is actually about tech. Most disagreements are about who gets to be right, and you can’t win those with benchmarks.
Write things down. Mostly for yourself, you will not remember why you made a decision two years from now. Keep a decision folder in every project. The first time someone reopens an old debate, you’ll thank yourself.
Side projects are the best way to learn things. Pick one small idea and finish it. Put it somewhere you own. It follows you between jobs. Your internal work doesn’t.
Drive your career, or someone else will drive theirs. Your manager is busy managing their manager. The engineers who don’t grow rarely lack ability. They lack a clear ask and the patience to make it twice.
Imposter syndrome doesn’t leave. Two decades in, the doubt still shows up every so often. You don’t kill it. You learn to keep working while it’s in the room.
So, let’s dive in.
ElevenAgents: Build a voice agent in under an hour (Sponsored)
With 400,000+ people in my audience, the same questions come into my inbox every week. Course details, coaching availability, and book pricing. I can answer them once, or a hundred times, but never fast enough. So I uploaded my FAQs and policies to ElevenAgents from ElevenLabs, and inside an hour, I had a voice agent taking those calls in 70+ languages. Update the doc, and the next call reflects it. No retraining.
Use code DRMILANMILANOVIC for 11% extra credits
1. The job is mostly debugging
This is the lesson it took me a long time to understand.
What I found out after a long time is that I spend a lot of time debugging stuff, not writing new code. Writing new code is much easier than debugging. It doesn’t just involve fixing problems; it also includes checking code, logs, etc, and doing the smallest possible thing to fix it.
With AI, the problem is that AI is not very good at this either. It can write nice code, but overall, it doesn’t know all the details of my DB, infrastructure, or earlier business decisions. It still cannot resolve problems on its own, so I need to jump in and fix them.
If you’re junior, learning to debug is a great choice. What I found out is that many developers don’t know how to use debuggers and profiles in their IDEs. This can be your advantage. Get fluent with stack traces, profilers, git bisect and the logs you actually read instead of grepping through. Learn to bisect a problem in your head before you bisect it in code.
However, the hardest thing for me to learn was not only about prevention but also about recovery. The best engineers I have met do not fear breaking something. They were calm under pressure. They find and fix the problem, document it, and proceed. Most mediocre engineers are afraid to break production systems.
“Debugging is twice as hard as writing the code in the first place.” - Kernighan’s Law
2. Code is a liability, not an asset
Every line of code is something you have to keep alive, maintain, and pay the price for running in production. Code is not cheap, code is expensive.
Writing code is something we always enjoy doing. This was the core of our work. And we did our best to produce the best possible code. We used patterns, good practices, algorithms, whatever we needed. And we enjoyed it a lot. Sometimes, we needed to delete some code too. However, none of this is free. Adding a new library, service, or abstraction comes with a cost.
This is one of the principles I wrote about in the Laws of Software Engineering book, because it changes how you read a codebase. Once you see code as a liability, the question stops being “what should we build?” and becomes “what’s the least we can build to make this work?” Most over-engineering will be removed when you ask this. So does most of the urge to rewrite something just because it offends you.
There’s a smaller cousin of the same idea: write for the next reader, not for the compiler. The compiler doesn’t care about names, structure, or comments. The next reader, who is usually you with less context six months later, cares about all three. Sometimes that reader will be you. Boring code is a gift to that reader. Clever code is a debt they have to repay.
Yet I know that saying this in most projects will not be welcome. Everyone wants to ship more features faster. With AI, we are at the top of tokenmaxxing. But maintenance cost doesn’t drop when volume goes up. It rises. The teams that figure this out before the bill arrives will outlast the ones that don’t.
We talked more about this here:
3. Solve the right problems, not just the assigned ones
In my career, I’ve seen many people who were constantly busy but never made a significant impact on anything. When the annual review comes, they are always surprised.
What is usually seen is that an engineer finishes their own ticket on time. They stay focused on what they do, stay quiet in meetings, mostly skipping architecture conversations because this is “not their job”. A year later, they have a clean Jira history and no real impact to show for what they did.
What I usually do is ask myself: what can I remove or fix for good if I could? Then I make sure that one gets done, even if it isn’t the most ticket-shaped item in the queue. Other tasks usually finish faster because there are downstream effects of the real problem.
Great engineers don’t do only what is on the ticket. They can describe their work as a much broader mission.
4. Plan before you code
I have a saying: every hour of design saves three hours of refactoring. And it is not changed with AI.
What I usually do is, before opening the editor, I try to understand the problem in depth. I read about it, try to understand the business decisions, and write them down to explain them to myself. Then I create (global) steps for implementing it. This will later become a doc.
Earlier in my career, I thought planning and meetings were procrastination. Later, I concluded that if I do this part well, coding will follow and be much easier.
5. Simple is (very) hard to achieve
In our industry, we tend to admire complex solutions. Everyone is obsessed with complexity. Kubernetes, microservices, name it.
The worst codebases I saw were written by people who over-engineered them. They add abstractions for flexibility that weren’t needed and generalizations for use cases that didn’t show.
I remember working on a project that looked too complex to me. Impostor syndrome hit me hard then. I wondered why this code is so hard to understand. Later, I realized I had seen most of the design patterns inside. Obviously, someone learned it and tried to squeeze them inside, making everything much more complex than it should be.
The best ones were something different. Someone did exactly what was needed, and the code was simple and clean to read. But making something simple is much harder than making it complex. That’s why most code isn’t simple.
6. Tests are a contract with future-you
I worked on many codebases with low test coverage, and even some with no tests. Such projects were problematic from the start. When you deploy such code, you can just pray that everything will work.
Every team I’ve been on that skipped tests “for now” never added them later. Then the bug happens on Friday at 2 a.m. The only variable is who pays for it and how loudly.
7. Bring solutions, not just opinions
I met many people who were good at stating problems. They could tell you everything about it and explain it many times. Yet, those who offer solutions are something rare.
You probably heard many times that “this project/architecture/framework/library/team/etc is bad”. It is easy to say that. But providing a solution, let alone fixing it, is not something you will often encounter.
That’s another reason why the best designs usually lose to the worst more often. The worst design will have an advocate driving it forward who can show what the next move will be. The better design will have an advocate who tells everyone all the reasons the other design will fail. Make sure you’re the advocate with the next move. Come armed with facts, a brief proposal, and a scaled-down design that you’ll actually accept.
8. Code wins arguments
I heard that early in my career, but didn’t understand it well until many years later. We had many technical discussions going on, sometimes even without any concrete decision. Sometimes decisions were bad, even though we discussed them a lot.
The solution to that was to stop discussing and build something small so that the next conversation can be argued about with real data. That one hour spent on the prototype is worth weeks of discussions.
And with AI tools today, this is even more amplified. Now, we can build prototypes quickly and see whether our intent is correct. When we have a concrete prototype, it’s no longer an abstract idea, and it’s much easier to understand and discuss.
If you’re stuck in an ambiguous technical debate that won’t converge, look for a way to prototype sooner. The room argues better when something is running.
9. Engineering is more about people than tech
We, people in tech, think tech is so interesting and that the best people are those with the most knowledge. Such people can argue endlessly about frameworks, architectures, languages, and system design. What I found is that some of those arguments are about the work, but a lot of them are about the ego.
The problem with this is that you cannot win arguments easily. You can prove every step, run benchmarks, write RFCs, but another person will keep finding reasons to disagree. This is because disagreement is not about the tech, but about who gets to be right.
To be good in tech, you need to learn the laws of human nature. In tech, nothing is about the tech, everything is about the people. Once you learn it, everything will become easier.
There is no best solution, but the one both sides can live with.
I went deeper on this side of the work in a conversation with Ethan Evans. The short version: best work doesn’t win on its own. Visibility, relationships, and being someone people actually want to work with shape outcomes in ways most engineers underestimate by a decade.
Read more about it:
10. Write things down. Mostly for yourself
I find writing things down to be my superpower, and I learned it the hard way. Software projects are usually very complex, and there are too many things to remember. Trying to keep everything in your head is possible, but only for a while.
I remember when one of our juniors asked me (then tech lead) about someone proposing a library we had earlier decided was not a fit, but I could not remember the details. The only thing I could say is "I think this doesn't work, but let me get back to you”. Then I spent two days trying to reconstruct why we decided in this way a few years ago.
Then I decided to keep a decisions/ folder in every project. One markdown file per non-trivial call: what we chose, what we rejected, why, and what would make us revisit it. Future me reads it more than anyone else.
11. Side projects are the best way to level up as an engineer
Side projects, side gigs, freelancing, or whatever can expose you to more experience, which is the best thing you can do
You don’t need a famous one. You need one small, useful thing you finished that lives outside your employer’s repo that you can point at.
During my career, I often did side projects, some as a freelancer and some for myself. All of this exposed me to new experiences and knowledge I could not have gained by only working for my employers. This multiplied my experience by a significant margin compared to people who only work on one project in their regular job.
Three of the best engineers I’ve hired had no big-tech badge on their resume. They had a side project I could read.
12. Drive your career, or someone else will drive theirs
We think our manager’s job is to take care of us and our careers. But actually, nobody is managing your career except you.
Your manager has a lot of things on their plate. Managing projects, their manager, other team members, and one’s own career. They don’t have time to think deeply about you. The promotion you assume will follow good work usually follows good work plus loud, specific advocacy. Decide what you want next. Tell the people who can actually give it to you. Then do the work that justifies the ask.
The engineers I see stuck at the same level for years are almost never less capable than the ones who moved up. They didn’t ask or make themselves visible. They assumed the work would speak for itself. The work doesn’t, in my experience. You have to speak for it.
13. Imposter syndrome doesn’t leave
This is something everyone in our industry has. I’m twenty years in, and I still feel it sometimes. This industry is so wide and deep that there are plenty of things you don’t know, and this feeling can make you believe you are not good enough for your job. And this is exactly Impostor Syndrome.
The trick isn’t to make it go away. It’s to keep working anyway. The people who look like they have it all figured out have, in my experience, the same private doubts. Just louder external signals. Take the doubt as a sign you’re still paying attention, not as a verdict on whether you belong.
Conclusion
None of these is something new you have never heard of. They don’t show up on a syntax cheat sheet or a framework launch post. They just keep working, which is the only test I have left for whether a habit is worth keeping on the list.
If you want the longer form of how these habits fit together, I collected 56 of them in Laws of Software Engineering, sorted across Architecture, People, Time, Quality, Scale, Code, and Decision-Making.
eBook 📘
Print 📕
Want to advertise in Tech World With Milan? 📰
If your company is interested in reaching founders, executives, and decision-makers, you may want to consider advertising with us.
Love Tech World With Milan Newsletter? Tell your friends and get rewards.
Share it with your friends by using the button below to get benefits (my books and resources).







