The differential between software engineering as you study at school and as you practice at work can be jarring.
At school I learned algorithms, data structures, and math. At my first job this knowledge was rarely directly useful, except for passing and conducting interviews. The foundation is critical when you're designing or implementing a new product or feature. But you spend far more man-hours constructing a building than you do architecting it. Unlike actual architects, software engineers have to construct what they design.
My first manager was incredibly thoughtful and patient as he taught me the trade of software engineering more or less from scratch. I've now had the opportunity to mentor dozens of junior engineers myself. I love this part of the job and I hope these lessons will be helpful to others starting out in the field.
Error handling
Maybe the number one thing junior engineers don't think about is what should happen when things go wrong. The user enters a malformed email address; the service you're connecting to is down; your code has a bug.
The most common pattern I see here is ignoring these possibilities entirely. The second most common is having a top-level catch block that catches everything and displays a (usually unintelligable) error message to the user.
The second pattern is acceptable, but only for errors that are truly unexpected - usually a bug in the code. In these cases it can be dangerous to continue processing the data; its better to bail out entirely and show the user an apologetic "Internal Error" message.
Any other error is expected. You should think through what these are and what the user experience should be in these cases. If some data entry is wrong, display the message under the input field. If an external service is down, retry a couple times, then tell the user to try again later.
The important thing is thinking through these possibilities. These bad cases are just as much a part of the UX as the good ones. If you don't have good error UX, you'll pay for this in by fielding support requests if you're lucky, or losing a user if you're not.
Readibility
Three important rules here:
- Code is written once and read many times
- Write your code for humans, not for computers
- Code is twice as hard to understand as to write
Another common junior pattern is writing code that is succinct across the wrong dimensions. The variable names are short or unintelligable, there's no white space, there's nifty syntactic sugar everywhere. This is optimizing for the source code to be short which helps no one - before the code is ever run, all your variable and method names will be replaced and all the whitespace discarded. These only exist for humans, so make use of them!
Make variable and method names descriptive, erring on the side of too descriptive. Break larger methods into smaller ones, with the methods having descriptive names. Write idiomatic code for whatever language and codebase you're using. Use comments judiciously - great code should be readible just from the variable and method names.
Code that is difficult to understand is a massive risk when someone needs to change or use that code later on.
Complexity
There are two types of complexity - accidental and essential. An example of accidental complexity is code that is needlessly convuled and confusing. This complexity is evil and should be excised entirely.
Essential complexity is unavoidable, as it sounds. A trivial but common example is when you have to interface with an external complex system. Your code will necessarily be complex in order to deal with this.
Your goal in these cases should be to isolate the complexity behind a clean abstraction. Authorization in the external system might involve a complex exchange; but you should be able to write a clean method that takes in a user and gives back a token. Databases are complicated; but if all you need is to store users, then you should be able to have a few methods that create, update, and delete a user.
If other people can use your code to interact with the complex system without worrying about any of that complexity (or even knowing it exists) you've done a great job.
Source control
Really we're talking about Git here. I was very daunted by Git when I first started and hid behind a Git UI for a long time. My advice is, don't do this. Git is an essential tool and not that complicated once you dive in. Get comfortable committing changes with good messages, creating new branches, and resolving merge conflcits.
Once you have good committing habits you start to get way more comfortable with aggressive refactors since its trivial to revert.
Pull Requests
Early on I was pretty careless about my PRs; I pushed the branch, opened a PR with a simple title, and figured the code would speak for itself.
My boss at the time told me to start thinking about PRs as the fundamental way I delivered value to the company. PRs are where your local code goes live. Its where your peers will evaluate your work. Its what your managers will review when you're up for promotion.
Its also an incredibly useful historical record for coders to understand the codebase. And that future coder might be you!
If the code was worth spending 10 hours on, its probably worth spending at least 10 minutes describing your changes.
Deploying
If you're lucky your company will already have good CI/CD setup that will test and deploy the code automatically. My recommendation here is that you take the time to understand the deployment and testing process. Eventually you'll have a bug that comes up during deployment, or that needs to be rolled back quickly. Better to have some understanding before that time.
Monitoring
Monitoring is incredibly important, and hopefully your company already has some good systems in place. The main task for juniors is learning how to log effectively. You can't know what unexpected bugs will come up, and sometimes a single log statement can be crucial to debugging an issue. But too many spoils the broth. This just takes some time to get a feel for.
A pattern I like here is releasing the initial code with a good amount of logging; then once the code has been deployed and stable for a few days, remove all but the essential logs.
When you release a big feature, take proactive responsibility for watching for anomalyous behavior. If (when) you do release a bug, it goes a long way to be on top of it before user reports start coming in.
If your company doesn't have good systems in place - push for them. No one ever got fired for saying we should have more alerting.
Craft
Take the time to learn your craft and improve your personal toolset. The earlier you do this, the longer they'll pay dividends in your career. This is broad, personal, and ever changing - I just added Cursor to my toolbox. But a few of my favorites:
- VSCode/Cursor
- Decreasing the keyboard delay speed
- Vim keybindings
- Terraform
- The pomodoro method
- Kubernetes
- AWS Lambdas
This is an eclectic list. The common theme is they made me better at my job of delivering software - by typing faster, understanding code faster, not repeating things in the AWS UI, deploying more easily, or breaking up my work day into blocks.
Its hard to prioritize adding tools since in the short term you'll ship less. But they'll pay off faster than you think, and ultimately this is the path to becoming a better engineer.
Closing
This list is continually updating. If you have thoughts or comments I'd love to hear them.