The differences between junior and senior software engineers

Posted on Sat 30 September 2023 in IT, meta

How can you tell whether you are dealing with a junior or a senior engineer, when it comes to software? And if you are looking to advance your career, how can you become senior? What are the distinctive traits you should be looking for?

Juniors vs Seniors

Not just surfacing issues, but also finding the best solution

This is the single skill that makes you senior. Decent software engineers should be able to spot issues before they blow up and make themselves apparent to the whole company, but the way you address those issues tells a lot about your level. The quality of your solutions makes you senior.

Removing instead of adding

Juniors are eager to write a lot of code. Their metric is quantitative: if I push a lot of commits, with a lot of code, I have achieved a lot. To them, there is no value in a more elegant solution: all that matters is a solution.

Seniors are wary of adding code, and instead seek to remove it. If they can fix the issue by taking away a bunch of code, they will happily do so. They understand that any codebase is complex already, littered by chunks left by people who thought "I'm just gonna get this fixed real quick and then come back to polish it appropriately" and never did so, that everything they write needs to be maintaned through the years and has the potential of turning into the very bloat they have to wade through every day.

Avoid using tech to solve non tech problems

Juniors have a narrow view: issues in your code are solved with more code. The answer to any problem comes from the skill they are most versed in. For an analogy, imagine having foot pain when walking:

  • a shoe dealer thinks you need better shoes
  • a surgeon thinks you need surgery
  • a homeopathy practitioner thinks you need natural remedies
  • a PT thinks you need to train your feet.

Everybody sees the problem from the narrow perspective of their expertise, but none of them stops to investigate and discover that, maybe, you are sitting too much and need to walk more.

While seniors understand that it's hard to be experts in many fields, they are eager to expand their knowledge to surrounding fields, with the assumption that that's the way to achieve a more holistic view of their own field.

Seniors understand what the solution to a code issue is not always more code. Maybe you can twist your deploy process so that the issue vanishes? Maybe what looked like a Very Smart Solution ® can now be simplified to reduce its complexity, and resize the issue you are facing? Maybe the documentation is not clear, and the customer is using the product in a wrong way?

Think long term

Juniors write their code, deploy it, and go on with their lives. The moment the code leaves their hands, it's not their problem anymore. They are happy to build a cathedral of code that people will look up to, and think how smart!.

Seniors think like they are going to be there in 10 years, having to troubleshoot issues and implement new features in the codebase. They reason like they have only themselves to blame, and are thus diligent in keeping things simple (rather than smart), and ensuring anybody with an appropriate background is able to pick up their work (because guess what, in 10 years they themselves are going to be those people).

I think it's quite hard to learn this if not the hard way. I find myself in the enviable position of having worked on the same project for more than a decade: the first version of my free WordPress plugin Post Pay Counter dates back to 2011. More than 10 years have gone past and, more than a dozen addons later, I am certain that I would not be where I am were it not for that. At the same time, you don't achieve seniority by only working long term on a single project. You need to branch out at some point, or you become like a thief who is very good with padlocks from one brand, but helpless with all others.

Can comment code

Juniors decorate their code with comments, similarly to the way they write unit tests (does the function return an int?). They often use comments either in places that are self-explanatory, or that should be self-explanatory (but aren't due to poor naming/needlessly convoluted behavior). They act like those novice film producers who think it's funny to interleave their movies with "ironic" remarks that they themselves make while pretending to watch the movie in an empty lounge. Something like "well, that hurt, didn't it [grin]" after the main character falls down a cliff.

# parse result
parse_result_frmt(records, result)

Seniors document the overall flow, the choices, and the reasons why something looks odd.

# open but not closed parenthesis are for matching calls with parameters
exclude_functions = [
    'date()', 'date.transaction(', 'date.statement(', 'date.realtime(',

How to become senior

To a large extent, acting as a senior will make you a senior, at some point. There's practical steps you can take to accelerate the process though.

Read other people's code, study it and question it

To learn to write high-quality code, you need to expose yourself to high-quality code. You can't know if a codebase is high-quality upfront, but even exposing yourself to any code is helpful. But don't just look at it – question it, ask yourself how you would have written code that serves the same purpose. Would have it been better? Worse? Under what metrics? Learn to find flaws in other people's code in the same way mathematicians look for flaws in your arguments.

Most importantly, you have to learn to read and work with code that was not written by you, in the same way as you must be able to read a book in English that explains idea that don't come from you. Look at others' codes, praise them, shout at them, accept them, edit them, tear them to pieces ... but learn to deal with them.

When I started working with Co-Authors Plus during my time at Automattic VIP, the codebase was obscure and messy in my head. You start debugging an issue and then jump from function to function, only keeping in your head the flow needed to debug that one issue. But I wasn't retaining much, I was gaining ~5% proficiency with each troubleshooting. It would have taken me ~15 troubleshootings to become decently proficient with the codebase. Then I wondered: what if I could hop there directly, at 80% proficiency?

And I printed out the codebase.

On physical paper.

Color, with syntax highlighting. Font size ~10.

I turned off the computer, took a pencil and colored markers, and it went like a breeze.

Suddenly I was there, knowing how all the code fit together and how each change would have rippled throughout. And then I started rewriting large chunks of the codebase with an understanding that would have taken me painful days to build just by flipping tabs in my editor.

To this day, I still print out my codebase when I have issues I can't easily troubleshoot, or when I have to do a major refactoring, or when I start collaborating on a project that is already built.

Come up with several solutions, and compare them

Next time you get a problem, come up with a solution. Then, before you implement it (because you do plan on paper before churning out code, don't you?), think of another solution. A different approach, either smarter or dumber. Compare them. List pros and cons of each, and argue for both. Come with a third if you can.

It's hard to develop a feeling for the best solution if you jump all-in as soon as you have an idea – you need to experiment with many ideas.

Iterate over your code

Next time you write a piece of code, leave it until the day after, then come back and improve it. Accept that you won't be able to churn out the best version of a function at your first try. Come back, change the variables names, think about the empty lines, the order of conditionals, the number of statements. Dissect your piece of code and improve it in any way you possibly can.

This is an example bit from the book Crafting interpreters, one that likely didn't come out as is at the first attempt.

static Entry* findEntry(Entry* entries, int capacity, ObjString* key) {
  uint32_t index = key->hash % capacity;
  for (;;) {
    Entry* entry = &entries[index];
    if (entry->key == key || entry->key == NULL) {
      return entry;
    }

    index = (index + 1) % capacity;
  }
}