On being a junior engineer
Originally published in Building LloydsDirect, I’ve added in some of the asides that I left out in the original piece.
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.
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.
While my boss and I were happy that this explained how we saw experience and seniority, there’s still a problem with this explanation: it doesn’t define exactly what a junior developer is to do in order to demonstrate that they’re ready to step up. But it did help show my juniors that they could focus on the work that was assigned to them and still feel confident that they were progressing in their career.
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.
Wearing many hats within a single project
Koutnik’s framework mirrors the lifecycle of a project. Projects are either spawned from some problem finding activity, or, in the case of innovation projects, the project starts with problem finding. Once the right problems are found, solutions are selected and then implemented.
In most businesses these activities are carried out different people, as suggested by Koutnik’s framework. But at Made by Many, software engineers (of all experience levels) act across all three of Koutnik’s roles in a single project. In early stages of a project, they work with designers and strategists and the client as problem finders. Later on, they solve these problems and then implement the solutions.
It’s probably more natural to work this way in product innovation, with engineers involved from day one. But I think that every developer, at some point in their career, will be in a situation where they wear each hat (irrespective of seniority).
If this is true, what does this tell us? It shows us that the nature of a developer’s work varies by which hat they’re wearing.
One hat requires precision and certainty and carries the penalty of bugs and overtime. Another hat demands thinking out loud without censor, which is only possible with the safety of knowing that you won’t be held to everything you say.
When this isn’t recognised, things go wrong. I’ve witnessed companies fail spectacularly by not understanding this.
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.
When the hat doesn’t fit
For a period, it was all the rage at digital agencies to rebrand their developers as “creative technologists.” Maybe this was meant to truly be a change in how technology was involved in the work, or maybe it just looked more impressive on the rate card. But simply slapping on a new title without changing how the work is organised or how engineers are involved in it is going to amount to bupkis.
If developers have little say in product decisions and are expected to just churn out code, you can’t one day slap on a new title, invite them to a brainstorming meeting and expect them to be creative thinkers overflowing with blue sky ideas.
In many agencies developers are at the very end of the line of work (a waterfall-y process where work flows from strategy/planning to design to development). In these organisations, the only part they play in the early stages of a project is to say if something is technically feasible. Or worse, their involvement is limited to providing “estimates” on how long a project will take before it’s fully even defined.
In either case, it’s in the developer’s interest to be as conservative as possible, lest their (terminally premature) evaluation later be used as a stick to beat them with. Any developer who’s been in this situation will have learned that they will be the ones who will have to pay the price of an overoptimistic estimate. Having been burned by optimism, is it any wonder that an engineer might adopt an attitude of caution or curmudgeonliness?
So let’s take a look at some concrete ways in which you can level up as a junior engineer.
What about progression frameworks?
What about all those career ladders and progression frameworks? Fundamentally, they try to reflect that a role involves many distinct skills, and that an individual can be stronger in some areas than others. Levels reflecting experience from junior to senior are too blunt, they don’t capture the nuance of progression or the skills involved.
Good progression frameworks give an idea of what skills you can work on with examples of how to develop them. Progression frameworks come in all sizes and shapes; some use levels and titles like “junior” and “lead,” others don’t.
When I worked at Made by Many, we wrote professional development handbooks for our three disciplines (design, technology and strategy/product management). One of my favourite things about the framework that we came up with is that half of the tracks are shared across disciplines — many of the skills we need to work together and succeed are common to all of us.
Many companies have published their progression frameworks, handily collated on progression.fyi. I recommend taking a look at a few of these in depth — though be warned: they make quite tough reading.
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.
This isn’t to say that all engineers progress this way. Some people — by either inclination or circumstance — end up solving “non-coding” problems very early in their careers. This is by no means a bad thing! However, this can, I’ve seen, lead to lingering feelings of technical incompetence. It may also not be rewarded.
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.
Patrick McKenzie has written an excellent career guide for aspiring software engineers. His main arguments are:
- you shouldn’t call yourself a programmer* — as this defines you by a specific output
- you should always understand the value you create and be able to articulate this
- your problem solving and soft skills are at least as — if not more — valuable as your technical skills
So, what’s a junior engineer to learn apart from coding? What pitfalls should they be mindful of? Here are a few that I’ve experienced or witnessed.
*Incidentally, McKenzie’s not the only one with beef with the moniker of programmer. Eric Sink wrote that you need developers, not programmer.
2. Know the limits of your knowledge and stay alert
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.
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.