Skip to content
construction

Today I found myself observing the construction site at Ljubljana's central train station. I've always taken their work for granted, but today I looked deeper. The station can't close, since it would severely affect tourism, transportation of people and goods, and much more. The work is carefully broken down, workarounds are implemented, and trains are logistically shuffled to minimize impact on everyone. These are good old-fashioned engineering practices, and they're universally applicable.

As I watched the workers coordinate their efforts, some maintaining existing infrastructure while others built new platforms, I quickly realized this is exactly what we do in software engineering. We face the same fundamental challenge: keeping systems running while simultaneously improving them. While building new features, we ensure minimal impact on users and customers, whether it's a database migration, refactoring a service, or updating libraries to resolve security vulnerabilities.

Early in my career, I naively believed engineers were measured by lines of code written. Now I strive for the opposite: do more with less. I still recall comparing code output with friends, celebrating 10,000 lines as a milestone, a metric that seems absurd in the age of AI.

Now I understand there are always trade-offs. You need to consider the customer, be aware of potential crashes in the middle of the night, and push back on certain ideas instead of mindlessly coding. These traits are even more important in the age of AI. Code creation is amplified, but good engineering practices are needed to tame AI's sheer output. What good is AI generated code if the feature isn't used or the application constantly crashes? None, I'd say.

Over the years, I've identified several core skills that define what it means to be a software engineer. These aren't about writing code—they're about thinking, communicating, and delivering value. Let me share some aspects of what I've learned.

System Thinking

As an engineer, you connect the dots and make systems talk to each other. This means understanding how different components interact, anticipating downstream effects, and designing solutions that work within the broader ecosystem. Throughout my career, I've integrated various third-party services, but my proudest accomplishment was generating a completely new app store backed by our product marketplace.

This project required true system thinking. I had to understand not just our marketplace API, but how the app store would consume it, handle errors, and present data to users. It was the first time I had to handle REST API calls — before that, I only had SOAP message exchange experience. It was challenging: lots of documentation reading, payload checking, and troubleshooting. Swagger and OpenAPI weren't widely adopted back then, so manual integration was the only way forward.

Communication

System thinking alone isn't enough, you need to communicate your intent clearly. I didn't have proper code reviews from the start, but when I did, I realized how important "communication" - or rather, intent — truly is. Code is read multiple times per day by fellow engineers, and the goal is to communicate to them what it does. Which brings me to my first code review: I felt like I was stripped to the bones, but I quickly realized how valuable feedback is for improving code quality.

Proper naming for functions and variables matters. Tests should be meaningful, not just achieve coverage. Principles like DRY and KISS should be applied when they make sense, not at all costs.

But communication extends beyond code. You need to write documentation, document technical decisions with RFCs or ADR documents—because code explains the "what," not the "why." Another aspect is asking meaningful questions, which can sometimes help you scrap an idea and save time.

Debugging

Even with clear communication, bugs happen. Finding bugs isn't always straightforward. Being able to attach a debugger and see what's happening "behind the scenes" has saved me many times.

I recall checking code once, going through a method from start to end to no avail. Only later did I realize a condition wasn't met, so the method was never actually called. I was certain the state change only happened within that method, which led me down the wrong path. This is where the debugger helped me realize my fallacy.

Time and Complexity Management

Debugging skills matter, but so does knowing when to stop perfecting and start shipping. I've rarely heard of anyone finishing a project, feature, or code-related task on time in the software field. It's a running joke that people like to share. This clearly shows there's always a need to properly manage time and complexity in software development. And doing this is not straightforward, you rely on experience to make it happen.

I recall working on a rewards program for a credit card with a tight deadline. It wasn't smooth sailing. Whenever we hit a roadblock, we either simplified our approach or defined functionality as "nice to have" to get the MVP out. Of course we ended up with technical debt, but that was the price we were willing to pay. And that's the time when I gained significant experience in practicality. In the end, you care about quick user feedback, not about crossing every T.

Comfort with Uncertainty

Managing time and complexity requires dealing with the unknown. There's significant ambiguity in software development, and you need to handle it. The real test of seniority, which still holds true, is how well you can handle ambiguity.

I realized and learned this as an intermediate engineer. I recall getting a feature request that left me overwhelmed. I didn't know where to start or what to look out for. Thankfully, I talked with a colleague who shared his wisdom: you need to break down big problems into smaller ones. Once you do this, clarity arises. Whenever I now tackle a problem, I break it down. This allows me to see smaller problems, ask great questions for context, and minimize the uncertainty that comes with software development.

Learning How to Learn

Breaking down problems is one thing, but you also need to continuously learn new domains, tools, and techniques. In the software industry, you must understand the problem to solve it. This is why engineers first try to attain domain knowledge. Only then do code, documentation, and everything else make sense. And the expectation is to pick things up quickly.

I still recall a conversation with my boss after my probation period ended. His words stuck with me: he thought I was slow at picking things up initially, but gradually realized how complex the product was and how hard it was to grasp all the business rules. In the end it came as no surprise it took me that long to understand the product.

Similarly, you need to apply the same approach when picking up new languages, frameworks, and tools. The underlying practices don't change, only the syntax or API changes. But you still need to adapt and acquire new knowledge.

You can only do this when you know how to learn, and there's no one-size-fits-all solution. Each individual has their own way of thinking and consuming knowledge. Over time, you refine the process again and again—it's a never-ending cycle.

Conclusion

As I've demonstrated through these examples, a software engineer's work entails far more than just writing code. This is where I see the difference between an engineer and a developer. Software engineering is not only about code, but primarily about constraints, trade-offs, and problem-solving — not which language you choose. Just like the construction workers at Ljubljana's train station who must balance progress with minimal disruption, software engineers must navigate complexity, communicate effectively, and deliver value under constraints.

This is why I don't fear AI; I use it to my advantage. All the skills I mentioned above will remain highly valuable, regardless of what people claim. A software engineer isn't defined by lines of code, but by how they get things done under specific conditions.

Published by...

Image of the author

Jernej Klancic

Visit author page