On being a junior engineer

On being a junior engineer

So you’ve got a job as a junior software engineer. Congratulations — it’s a fun gig! At first you’re just chuffed that you’re in this new role. But at some point you will inevitably ask: what do I need to do to not be a junior any more? Chances are — despite career ladders and progression frameworks becoming more common — your organization won’t have a clear answer.

Being a junior means you lack skill or experience. If you’ve moved from another field, you may only be new to software development. But if this is your first job, you have to learn both your craft and the rules of worklife in general. This may seem like an obvious point, but it’s significant: knowing how to work in a team, being able to understand a task you’ve been assigned, taking responsibility for it, and communicating your progress are just as important as developing your technical skills.

I’ve fielded the question of what it takes to “not be junior” several times in my career. I don’t have a perfect answer (and it varies from company to company), but I do have some advice and a few thoughts about the nature of the job to consider.

One thing to note is that I’m writing from my experience in web development, and I use the terms engineer and developer interchangeably.

How many languages do you need to know to get promoted?

In my first role as a manager, I was given the least experienced members of the engineering team to look after. Looking back, I’m not sure it was the best idea to give a first-time manager the responsibility of looking after people early in their career. At the time I did appreciate that I wasn’t facing the prospect of managing senior developers of comparable or greater experience than I had.

At one point, I noticed that something funny was going on. A few of my junior developers had started asking for backend work — work outside their expertise. As web developers, our juniors typically started out doing front-end work, using HTML, CSS, and basic Javascript.

As pleased as I was with their desire to learn and expand their capabilities, I was perplexed by this sudden interest. I soon found out what was going on — they’d been talking about what it took to be promoted.

They had gotten into their heads that you needed to know at least four languages (though there was some debate over this number — or, more specifically, what counted as a programming language).

It colours my cheeks even now, thinking about this whole situation. While I was satisfied with how they were progressing — each at their own speed on their individual paths — I hadn’t given them any explicit criteria to work towards.

In the absence of better instructions, they had — perfectly reasonably — looked at their more senior colleagues and concluded that they all had experience in server-side languages; this must be the requirement for advancing!

Seniority = independence + greater responsibility

I went to my boss and we set out to describe how levels of seniority differed from each other. The model we came up with was based on concentric circles of responsibility.

Given a well defined task and solution, a junior can be expected to complete this task. The task definition might include:

  • the desired outcome
  • any constraints or integration points with the broader system
  • an overview of how to achieve this
  • the components, packages, plugins or libraries needed
  • potential gotchas that might come up
  • defined checkpoints on when to consult with a more senior person
  • any salient tutorials

With these ingredients, a junior should be able to semi-independently work through the task. They are free to ask for help at any point, of course. This is kind of like what HelloFresh or Blue Apron give you: a box with a recipe and the ingredients to make it.

Over time, juniors should be able to work more and more independently as they build up their confidence and experience. They can then take on jobs with fewer and fewer explicit items from that list, dropping the bottom ones first. At this point they are able to take responsibility for a desired outcome by doing the technical research and design themselves.

A more experienced developer can take responsibility for a desired outcome by doing the technical research and design themselves. They will still need to be provided with the desired outcome and an understanding of the relevant constraints.

A developer can design an entire small or medium-sized system (or part of one). To continue with the cooking analogy, at this level you can follow “professional recipes” (notorious for vague quantities or for using terminology and techniques that they don’t explain) or cook without a recipe, improvising with ingredients and quantities and be able to imagine how the combinations will taste.

As a developer gains experience, they are able to see further afield: both into the future — by evaluating approaches and anticipating potential pitfalls — and across the boundaries of their own work — by communicating with and supporting other team members.

A senior developer helps define the desired outcomes and elicit the system’s constraints. They either lead or oversee the design of a system and define the boundaries of the work, dividing it up between developers.

In cooking terms (and I admit this analogy is getting stretched), a senior can not only plan a menu and devise delicious recipes for others to follow, they can also choreograph their work with other people making their own dishes, all timed to be served together.

Note that none of these responsibilities are solely held by one person, regardless of seniority. A junior developer can and should help define the desired outcomes, just as they can help teammates solve a problem.

So getting promoted from junior developer has nothing to do with how many languages you know, it’s about being able to take responsibility for an outcome and being able to figure out how to get there.

Hats and ladders

Years after I’d first been asked what it takes to no longer be junior, I heard Randall Koutnik speak about the problems of defining seniority. (There’s also a blog post if that’s more your style.)

Rather than talking about juniors or seniors, Koutnik proposes to use the titles: solution implementer (junior), problem solver (mid-level) and problem finder (senior). Conceptually, they dovetail nicely; each level outputs the necessary input for the other: Finders find problems that Solvers solve for Implementers to implement.

Koutnik’s model doesn’t just describe seniority, it also describes the nature of a role. It’s a much richer way of viewing the different shapes of engineers at companies. It’s also way less limiting than most progression frameworks or career ladders that limit themselves to listing or highlighting the distinct skills and attributes that make up a role.

If you are a junior engineer you are most likely on the implementer side of this range when compared to your staff engineers, who will most likely be defining problems and solutions to help you.

So let’s take a look at some concrete ways in which you can level up as a junior engineer.

1. Look beyond just the code

It’s been said that going from engineer to engineering manager is not a promotion, it’s a career change. The skills you need to succeed are different from the ones that got you there. But the more that I think about it, the more I think this applies to moving from junior to senior engineering roles as well.

This, really, is what Koutnik’s model of seniority implies.

It seems plain that early in any career, the focus should be on learning the fundamentals and mastering the tools of the trade. To use a sports analogy, it’s only once a footballer has mastered the basic skills of ball control that they can look up and see the game around them. Or take pool: a beginner only thinks about their immediate shot. But to play well, pocketing the ball you intended to isn’t enough, you have to consider where the cue ball and other balls will end up after your shot.

For software engineers, this means learning how to write code and solve problems with code. Once this has been mastered, an engineer can afford to shift their focus to making sense of the bigger picture — be this teamwork, systems architecture or product/business.

In code, this laddering up of focus means going from figuring out how to implement a given feature to considering how it will affect the codebase at large and understanding how the code may change over time. But it applies outside of code, too: a growing engineer must learn how to plan work, document their changes, and to be able to communicate all of this at the right level and to the right people.

Not everyone will want to move up the abstraction layers of problem solving. If you love writing code and would prefer to just get well-defined tickets to implement or problems to solve, that’s fine! Just know that this might limit your career progression.

2. Know the limits of your knowledge and stay alert

S-curves are like rounded stairs

Learning is stepped, a non-monotonic curve made up of a series of S-curves. (Yes, I googled that.) When you’re really learning, it’s hard going — you’re climbing up the steep part of the learning curve. Once you’ve reached the local summit, the curve levels off. Zooming along this flat plateau feels like you have superpowers. All of the sudden everything is easy!

I remember working with a brilliant young developer. He was passionate about his craft, creative in his approach, and he absorbed new technologies and techniques like a sponge. His enthusiasm was infectious. He regularly shared interesting things he’d come across with the team, and he went out of his way to help his teammates. And then, seemingly all of a sudden, he started making mistakes. Things slipped through: a security test revealed a pretty glaring XSS vulnerability in his approach; his “quick little tweak,” made in production, crashed the site.

He’d developed speed blindness. His skills were formidable, but he was going so fast — he was so confident in his abilities — that he was taking on too much, skipping good practice.

He hadn’t yet learned the dangers of going too fast. Part of being responsible is recognising that you can’t do it all alone. Being a (more senior) developer means checking your work, going through the proper steps, even when you’re sure it’s fine. Especially before pushing that minor tweak to production. I’ve made that mistake enough times that I take pride in having the patience to check it again.

3. Learn when to ask for help

Part of a junior’s dilemma is Rumsfeldian — you don’t know what you don’t know. You also don’t know what’s good enough. When you’re going about your work, pay attention to:

  • When is the right moment to ask for help? If you persist a bit longer before asking for help, would this help you learn on your own? Being stuck too long before asking for help is the much more common problem, I’ve found.
  • Where to start? What are the first things to do before starting to code? Do you understand why this work is important? Does some part need to be delivered earlier? Are there certain patterns or constraints that need to be considered?
  • When to stop? Related to asking for help, knowing when to stop is important! When is good enough good enough? I knew a developer who would write the same algorithm three times and pick the best one. If this is quick and done as a way to better understand the problem (and solution), fine. But most times this is simply wasteful.
  • When to change tack? If something isn’t working, the breakthrough might be just around the corner — or it might be that you need to abandon this approach and try something else.

I can see why, when you feel you’re constantly having to ask for help, not asking for help can seem like a sign of progress. If you’ve become less reliant on help, do exalt in your newfound independence! But not asking for help should not be a goal in and of itself. An engineering culture which discourages asking for help is a dysfunctional one.

4. Learn how to make mistakes well

Inevitably, you will make a mistake. This is part of learning. Sometimes you’ll make serious mistakes, mistakes with real consequences. When this happens, you will have the measure of your mettle. Do you hide your mistakes? Do you blame circumstances or someone else? Or do you take responsibility, notify your manager or team or client? Do you think about how you can avoid making this mistake again? The measure of an individual is not how few mistakes they make but in how they handle the ones they do.

“Only true champions come out and show their worth after defeat”
—Sir Alex Ferguson

I remember hearing a story of a poor engineer who messed up some server configuration. It was the end of the day and he was explicitly told to stop trying to fix it and go home. Despite this, he continued making changes late into the night, ultimately making the issue much, much worse. He was fired — not for the initial mistake, but for ignoring the order to stop (and causing further damage).

5. Learn how to make technology choices

Making sensible technology choices involves being able to research and evaluate different alternatives. This means asking and answering questions such as:

  • Have you canvassed your peers? Is there a preferred approach or tool we use?
  • What trade-offs might we be making if we use this plugin, library, framework or approach?
  • Is there a design pattern that’s appropriate to use? Should you build it from scratch or introduce an existing component? What dependencies will this introduce?
  • Is this likely to change in the future? How often and how soon? How hard will it be to change?
  • Will another developer (or future you) understand this? Can I explain my thinking?

To help interrogate technology choices, a former team of mine created a canvas to help think of all the angles and document the decision.

6. Learn to use abstraction, reusability and optimisation carefully and considerately

Have you ever used a big and fancy word — only to discover that you got it spectacularly wrong? Either thinking it means something it doesn’t or by mangling the pronunciation horribly? I’ve done both.

Trying to follow some coding principle or philosophy that you don’t quite understand is like that. Only with potentially much longer lasting consequences. You (and your team) might have to live with that mistake for quite some time.

DRY, code reusability, OOP, TDD, clean code… what these all have in common is that A) they are preached with pious fervour and B) in the hands of a novice, they can cause more harm than good.

I’m not saying that these aren’t valuable principles or that you should never learn them, it’s just that software engineering involves lots of trade-offs, and dogmatically adhering to “best practices” can serve as an unnecessary distraction and overly complicate your code.

There’s a balance to strike between stretching yourself, which is fundamental to learning, and overreaching, which can end in a hot mess.

I remember one of my junior developers — I’ll call him Herbie — trying to write some UI interactions as a reusable component. While his ambition for reusability was commendable, the problems he kept hitting were that he was abstracting the wrong parts. Even worse, because his component was being used only once, his attempts to make the behaviour configurable was over-complicating the code for no good reason!

My advice to Herbie was: solve it first for the simplest case. Hell, hardcode it first! Then, if need be, abstract the necessary variables. Build exactly what you need before deciding which parts should be configurable. You will be tempted to think of all the possible ways the component could be used — resist these temptations! YAGNI.

My rule of thumb is that you shouldn’t try to “systematise” something until you’ve built it three times. By changing as necessary rather than through anticipation, you’ll end up with much less (unnecessary) complexity.

In general, look backwards, at use cases and code that already exist, to find opportunities for abstraction. Looking forwards, at potential future use cases is hard, because, well, predicting the future is hard.

This is like the rule of premature optimisation: don’t do it! Even with years of experience, I still think that trying to anticipate future behaviour leads to confusing complexity as well as rework. Rework might be unavoidable, but unneeded complexity should be.

Making something do only what you need it to is a great guiding principle! Unbounded flexibility is like a block of marble: it contains the potential of innumerable sculptures, but chipping away the parts that aren’t needed is a lot of work.

7. Learn to estimate work

As a junior, I think you should practice estimation. This practice goes hand in hand with decomposition and communicating your progress. Being mindful of incremental milestones, what tasks are involved, how well you know the things that need to be done — these are important factors in how long it will take you to achieve the goal. However, juniors shouldn’t be expected to estimate their work and be held to them.

What you should focus on is reasoning about the task at hand.

You should be working with another team member to figure this out. They should help by asking about any angles or aspects you may not have considered. You can then make a guess — sorry, estimate — of how long you think you’ll need. If it’s more than a day, you should try to figure out where you’ll be at lunch time or the end of the day. What’s important is not that you get to where you said you would, it’s that you check in, reflect, and report your progress.

This reflection is key to building up an understanding of estimation.

When a developer falls behind, one natural response is to try to code themselves out of their hole. “I’ll just work a bit longer today and make up for lost time,” they think. This temptation never goes away. I’ve seen developers of all experiences fall into this trap. I’ve done it myself many, many times.

Occasionally this can work. But the risk is that if you can’t make up time (which you shouldn’t have to — it wasn’t that you were slow, it’s that the estimation was off), you rob your team of the chance to react and respond: this is new information that can be factored into the plan. Maybe this delay means we need to shift the release day. Maybe it means we need to cut a feature from the release. Having early notice makes all of these things much more manageable!

Estimation has been studied since the dawn of software engineering. There are many different ways of approaching estimation, and there are those who believe it has no value. (#NoEstimates)

I find estimation incredibly useful, but as information to help the team agree on trade-offs and to make plans. But when estimates are used as sticks to beat developers, they become counterproductive.

Estimation, despite all the different metrics that have been proposed over the years, is a calculation seemingly done from the gut. This means it’s a guess informed by experience. As such, it’s something that you just have to do to get comfortable with.

I will mention one mistake that only experienced developers tend to make: underestimating familiar or routine work. Veteran engineers recognise that novelty or uncertainty will increase the riskiness of a given endeavour, and factor this into their estimate. But there is a limit to how fast you can do something, even if you’ve done it before, time and time again. Think of boiling eggs: it doesn’t matter how many times you’ve done it, deep experience won’t speed up the actual boiling process.

8. Know thyself, but also know thy boss

When I first became a manager, I found that working with some people felt natural and easy, but working with some others... wasn’t. Why was this? Most likely because the first group thought like me, so it was easy for me to understand and empathise with them. Learning to understand and support the latter group — those who thought differently to me — was something I had to learn.

In Managing Humans, Michael Lopp talks about the difference of reporting to “big picture” managers and detail-oriented managers. If there is a difference between you and your boss, and neither party recognizes this, you will face frustration and disappointment on both sides.

Over the span of your career you will have good bosses and bad bosses. Subjectively speaking, one person’s bad boss might be a good boss to another person. Your boss has a huge impact on whether you will be recognized or promoted. Understanding their expectations and their (subjective) image of you is a vital factor in your success. I’m not saying that this should be entirely your responsibility, or even that this is fair, I’m only saying that this is something you should be aware of.

9. Understand the product and the business

In order to build the right thing — and then build it right — you need to understand not only the context and the desired outcome of your work but how it fits into the bigger picture of the overall business. At any given time, this might mean considering the value of the feature, the needs of the users who will use it, the constraints of the project, your team’s engineering principles, or the values of the company that you work for.

For example, if your product is trying to find its product-market fit, speed of delivery might trump longevity. In other circumstances, robustness or the ability to scale might be more important than how fast it can be delivered. Often good enough is good enough.

Things you should seek to understand:

  • How does the business make money? What are its most pressing challenges?
  • How do you win customers? How do you lose them?
  • How does your work fit in with this?

You need to understand these in order to make the right trade-offs. Without this knowledge, you run the risk of investing time and effort where it won’t be appreciated. It doesn’t matter if you’re producing the cleanest code in the world if you’re working on a feature that never gets released or that isn’t valuable to the business. Don’t rearrange the deck chairs on the Titanic!

Inversely, the surest way to succeed is to be attached to “profit centers”, where the business makes its money.

Helping my team understand these considerations is a big part of my job as a manager. This is especially important as you become more senior. At the high end of the individual contributor track (the most senior positions who aren’t managing people) are principal engineers who are explicitly tasked with delivering impact beyond their own code output.

Which comes first: promotions or growth?

Promotions and pay rises are important. There are things that will get you promoted and there are things that will let you grow. These will not always be the same things.

Your career will have its ups and downs. I’ve had periods in my career when I successfully faked it till I made it. (It is due to my privilege that I was able to do so.) I’ve also had periods when I doubted myself so deeply that I couldn’t imagine ever being employed again. Few escape occasional bouts of impostor syndrome.

Ultimately, what will sustain you on your journey is knowing yourself, what you find rewarding, and seeking out positions that allow you to do this kind of work. Sometimes growth will require you to learn new habits or to change what gives you a sense of achievement. This will be difficult and painful. Knowing your own strengths and what makes you tick will get you through these hard times.

And every now and then, when you’re up for it, cast your eyes to the horizon.

Tl;dr: “It’s not just about code”

That’s it. That’s basically my spiel for how to succeed and progress as an engineer. There’s more to it, of course, and many other ways to make it, but these are the things that I’ve seen and told my junior engineers over the years. A comment that I’ve heard many times over goes: “I never realised there’s so much more to being an engineer besides coding!”

My thanks to Matt Vagni and Łukasz Sągol for their help editing this. It was longer before they came along.

Any questions or feedback? Want to hear about new posts?
Follow us on @BuildingLloydsD
© 2024 Metabolic Healthcare Ltdlloydsdirect.co.uk