What Nobody Teaches Junior Engineers
Every junior engineer I've mentored has known how to write code. Almost none of them have known how to craft software. Those are different things, and the gap between them is where most of the early-career pain lives.
I know that gap firsthand because nobody taught me this stuff either. I picked it up the expensive way, through production incidents and merge conflicts and builds I broke without understanding why. It took years. And when I started mentoring engineers myself, I kept seeing the same patterns, the same avoidable mistakes, playing out on every team I joined. I've mentored over 45 engineers at this point, different companies, different stacks, and the gaps are almost always the same.
The lifecycle nobody explains
Most junior engineers don't understand the Software Development Lifecycle. Not the textbook definition, but the practical one. They don't know what happens before a ticket reaches them or why a release gets held. They've never watched a requirements document evolve. They don't understand why a staging environment exists or what a deployment pipeline is supposed to validate.
This shows up in the work in ways that are hard to trace back. A junior who doesn't understand the pipeline writes code that works locally and breaks in staging, or even on another machine. A junior who's never seen how requirements change writes the feature they imagined rather than the one the customer needs. A junior who doesn't know what an incident review (or postmortem) is will make the same mistake twice because nobody showed them the mechanism for learning from the first one.
When I onboard a junior, SDLC is day one. Not as a lecture, but as a walkthrough. "Here is how a feature goes from idea to production on this team. These are the stages. This is where your code enters the process, and this is everything that happens to it after you push. That single conversation changes how they think about everything they write, because they stop seeing their job as "write the code" and start seeing it as "deliver a working piece of a larger system."
Stop coding the happy path
This is the one I end up repeating the most.
Junior engineers write code that works when everything goes right. They handle the case where the API returns a 200, the user enters valid input, the database is available, and the network doesn't drop. And then they call it done.
That's not software. That's a demo.
I call it CYA coding, or "cover your ass." The framing sticks in a way that "defensive programming" never did with the engineers I've worked with. The question isn't "what does this code do when it works?" The question is "what does this code do when something goes wrong, and will I be able to figure out what happened?"
That means error handling that actually handles the error, not catch blocks that swallow exceptions and move on. That means validation at the boundaries, not just the UI layer. That means logging that tells you something useful when you're reading it at 2am because a customer reported a blank screen and you have nothing else to work with.
I had a junior ship a feature that worked perfectly for three months. Then it started silently writing bad data to the database with no logs, alerts, or error messages anywhere. The catch block was empty. Literally empty. He'd put it there because the compiler required it and then moved on to the next ticket. Three months of corrupted data before anyone noticed. The root cause was a block of code that existed purely to satisfy a syntax requirement, and nobody had ever told him that the catch block is where the actual engineering happens.
The happy path runs itself. The unhappy path is where your code will actually be judged.
Git is not a save button
Most juniors learn enough git to not lose their work. Clone, add, commit, push. They treat it like autosave. Commits called "fix" or "update" or, and I've seen this more than once, "asdf." One massive commit at the end of the day with every change lumped together. No branches. No PR descriptions. No sense that anyone will ever need to read this history. It makes my eye twitch just writing about it.
Git is a communication tool. The commit history is a record of decisions, the pull request is a conversation, and the branch strategy is how a team coordinates without stepping on each other. None of that is obvious if nobody explains it.
When I mentor juniors on git, I start with commit messages. A good commit message answers "why." Not "fixed bug" but "handle null response from user service when account is deactivated." Six months from now, when someone is running a git blame trying to understand why a particular line exists, that message is the difference between understanding the decision and having to reverse-engineer it from context.
PR descriptions matter even more. A PR that says "implements feature X" tells the reviewer nothing they can't see from the diff. A PR that says "chose approach A over approach B because of constraint C, and here's what to watch for in the integration" turns a rubber-stamp review into a real conversation. It makes the reviewer better at their job and the author better at explaining their thinking. And it creates a written record you can point to later when someone asks why it was built that way. That's the CYA principle showing up again, in a completely different form.
Branching habits are the part most juniors don't learn until they've caused a merge conflict or branching mistake that takes an afternoon (or, at one previous client, 6 full releases) to untangle. Short-lived feature branches. Pulling from main frequently. Not letting a branch drift for two weeks while the rest of the codebase moves underneath you. These are collaboration habits, not git commands, and they have to be taught explicitly because the tooling alone doesn't enforce them.
Also, use a trunk-based model, for the love of all that is holy.
The rest of the toolkit
There's a set of tools and skills that sit outside the code editor that juniors rarely get shown but use constantly once they know they exist.
Terminal is not just for running builds, but for navigating a codebase, searching logs, running scripts, inspecting processes. A junior who can grep through a log file to find an error pattern will diagnose issues in minutes that would take an hour clicking through a dashboard. I've watched engineers spend 20 minutes navigating a logging GUI to find information they could have pulled with one command.
Debugging tools go beyond print statements. Use breakpoints, watch expressions, and step-through execution. The ability to pause a running program and inspect its state is one of the most powerful skills a developer can have, and a surprising number of juniors have never touched a debugger because console.log was the only feedback mechanism they were ever shown.
When it comes to CI/CD awareness, juniors don't need to configure the pipeline, but they need to understand what it does. They need to know that the build they just pushed is running automated tests, that a red build is their responsibility, and that the linter is not a suggestion. I've seen juniors push broken code, see the build go red, and walk away because they assumed someone else would deal with it.
Finally, when something breaks in production, where do you look? That's where monitoring and observability come in. What does the error tracking dashboard show? Where are the logs for your service? What does an alert mean when it fires? Not because juniors are on-call on day one, but because understanding the production environment changes how you think about the code you write for it. An engineer who has seen a production error trace handles error states differently than one who hasn't. That exposure has to be deliberate.
What this actually produces
The skills matter, but the real outcome of good mentoring is an engineer who understands that their code exists inside a system, that the system includes other people and other services and a deployment pipeline and users who will do things nobody anticipated, and a future version of themselves who needs to figure out what went wrong with whatever information is available.
That understanding doesn't develop on its own anymore. AI tools let juniors generate working code faster than ever, which is genuinely useful, but it also means the natural learning mechanisms that used to be built into the process of writing code, the debugging, the struggling, the failing and figuring out why, get bypassed. I spent 20 years developing those skills before AI came along, and I'm seeing most junior engineers today missing out on those opportunities. The shortcut to working output skips the experience that builds judgment. That makes structured mentoring more important now than it was five years ago, not less.
And this connects to something I feel strongly about when it comes to hiring. Given the choice between a candidate with a wealth of existing knowledge and one who knows less but has shown a real drive to learn, I will always hire the second person. Knowledge has a shelf life. The technologies change, the patterns evolve, the tooling shifts underneath you. But an engineer who is genuinely curious, who actively seeks out what they don't know and closes the gap on their own, that orientation compounds over an entire career. You can teach someone a framework. You can not teach them to care about getting better.
I don't have a curriculum I hand out. What I have is a set of conversations I keep having, and this post is most of them. If your team is hiring junior engineers and the onboarding doesn't explicitly cover the lifecycle, the defensive mindset, and the collaboration tools, those gaps will fill themselves in eventually. They'll fill themselves in through production incidents, merge conflicts, and an engineer who still can not explain why their code works on their machine but fails in staging. And the cost of that education, when it happens on its own, is always higher than the cost of teaching it on purpose.