Tips for New Developers
During my career as a software engineer so far, I have had the opportunity to work with a large variety of junior developers. I’ve been able to work with fresh graduates, summer interns, and year long placement students from top universities, which has been both exciting and at times a little challenging! I very much appreciate having had these opportunities - there is a lot to be learned from fellow developers both young and old, and I’ve enjoyed being able to share the wisdom I’ve learnt from working in the real world to those just starting out.
There are however some mistakes that I see new developers make time and again, and some of which I remember making myself! I thought I’d share some advice to those developers coming into their first software engineering job.
Don’t Fear the Code Review
At university, rarely was my code for assignments properly reviewed. University programming assignments are far more focused on demonstrating specific algorithms and techniques than looking at general good engineering practice, and group projects are usually far too disorganised.
So when you land in your first software engineering role, you’re kind of unprepared for having someone else going through your work with a fine tooth comb! I know this is how I first felt - code review was a daunting process. Every comment is an accusation of failure, a personal attack, a leap towards the inevitable discovery that actually you’re not qualified for the job!
None of this is true!
As a supervisor who is responsible for reviewing the code of new staff, it doesn’t matter to me if I need to make suggestions for changes. Programming has a lot of flexibility, and there’s always more than one way to solve a problem, structure some code, or even just format code - and ideas on what’s best will change from team to team. It takes some time to get into the swing of a new team’s engineering practices, so a good supervisor will understand that time is needed to adjust.
The only thing you need to look out for is that you learn from previous suggestions. If your supervisor keeps noticing the same problem, they won’t just give up reminding you - but it will leave a bad impression!
Sidenote: Don’t Be (Overly) Apologetic!
I have supervised junior developers who have written their first merge request, and as soon as I leave a suggestion during review, they’ll message me with an apology! While the sentiment is appreciated, it’s usually unnecessary. Your reviewer isn’t angry or disappointed - mistakes are accepted and accounted for - that’s why code review exists!
It’s Not a Race
Some interns I’ve had will be assigned an issue to work on, and then race to get a merge request in place as soon as possible. While the enthusiasm is appreciated, it’s definitely possible to go too fast1.
A well thought out, well tested solution is always better than something raced out the door just for speed’s sake! Make sure you take time to understand the problem first, and think about how what may at first seem like a small change might affect other parts of the system that interact with the part you’re working on.
It’s more efficient overall if you strive to achieve a good solution first time - your supervisor will thank you for it!
Learn Version Control; Use it Well
Version control is so important and so widespread, that almost every new hire will know the basics, but using it well isn’t always obvious. When commiting, you need to be thinking about the commit from the perspective of other developers.
Give Good Messages
Make sure your commit messages have a clear and concise summary line of what they do. If you need to give more detailed explanation (and that isn’t covered in the actual code changes you’ve made!), then you can use the extended commit message space. Usually, the extended space is best used to cover the why of the change. You might also want to give some details of the how - but really your code and comments should do the talking on this front!
If another developer needs to look at the history to find out why something behaves the way it does, then the summary line is going to help them identify which commits are relevant.
Then, once a likely commit has been found, the changes and the full description will help confirm if these changes are relevant, and point the developer to why that change was made, which might answer their question!
Make Focused Commits
Making sure your commits are focused can be very helpful - multiple commits that do one thing each are easier to work with than one large and muddled commit. Simple commits that do one thing (or a small number of things) make history and blame output much easier to understand. It also makes your life easier when trying to write good commit messages. A description of a commit that does everything doesn’t tend to fit into 50 characters or less (or whatever your team’s ideal length may be)!
Another benefit of focused commits is it can make undoing changes that turn out to be unwanted much easier. If I have 2 commits, one which fixes some formatting, and the other which introduces a new feature, I can easily revert the second commit whilst preserving the formatting changes. If both changes happen in a single commit, I’m forced to preserve the formatting changes manually.
Of course, there is a balance to be sought after - commits are like comments, and you can go too far!
Make Focused Merge Requests
Making sure your merge requests (and by association, your feature branches) are focused is also very helpful, primarily for the reviewer. If you try to fix multiple bugs in a single request, it makes it a lot harder for the reviewer to understand what was actually required for what task.
For example, perhaps while implementing a new feature, you notice a bug in a neighbouring class that seems to have slipped the net, and you can find no mention of it in the bug tracker2. You could go ahead and fix it while you’re there, but then at review time your supervisor is going to be confused why you’ve had to edit seemingly irrelevant code. It’s often easier if you either:
- Make a new branch, and fix the issue there, which has two benefits:
- The fix may be able to be deployed much quicker
- If you’re fixing a bug that nobody has noticed, then you should also be adding automatic tests to cover it that will only make the original merge request larger
- Report the bug to the issue tracker, and leave it for someone else:
- It allows more concurrency in the development process, getting your original work item out the door faster
Of course, your supervisor is best placed to tell you which of these options is better, as the one who is likely involved in both reviewing and prioritising developer time. They might know of an imminent replacement of that broken code, which means fixing it would be a waste. They might come back and tell you to fix the bug in the same MR, if it’s suitably small and low priority enough!
Learn The Useful Tools
Pretty much everyone has had enough exposure to git
to know to git add
, git commit
, and git push
to share their code on GitHub, but it’s useful to learn about some more tools:
git blame <path>
,git log <path>
, andgit show <commit-ref>
are crucial for seeing who else has worked on a file, and why certain changes were made and when.git revert <commit-ref>
will make a commit that undoes all of the changes in the specified commit, which is useful to avoid rewriting history after a mistake.git checkout
, andgit reset
, which can be useful for unstaging changes that you’ve decided against making.- The
-p
or--patch
flag is also very handy, and can be used in conjunction with commands likeadd
andcheckout
, to allow you to only operate on a selection of changes.
The following tools are also handy, but will rewrite history, which can be problematic when working collaboratively:
git commit --amend
will write your currently staged changes into the previous commit.git rebase
is an extremely powerful tool that can be used to cleanup your commit history, but can also ruin your git repo if used without care.- Similarly,
git pull --rebase
provides a shortcut for rebasing your current changes on top of freshly fetched remote changes, but can also make a mess if you don’t understand what it does!
Rebasing can be complicated, but it’s an important tool in your arsenal for keeping your feature branches conflict free and the history tidy. Of course, if you plan ahead and use git wisely, you can reduce the need for rebasing, which is usually a good target to aim for!
Get Your Tooling Setup (Efficiently)
You start a new job, you get given a computer, you do all the admin, and then you get assigned some coding task to start you off. Depending on what kind of work you’re doing, you might be setup with an IDE, or you might be given some more freedom on how you work.
It’s worth spending some time before you dive into working on your first task to get your development environment setup. Simple things can be very helpful, particularly with regards to code style. Consider configuring your editor to:
- Highlight or automatically delete trailing whitespace
- Convert tabs to spaces (or vice versa, depending on the guidelines!)
- Enable version control integration, to highlight uncommitted changes, and provide history information
- Tidy imports
- Run linters and tests
Some fairly quick setup can save you a lot of time in future, and help prevent small issues from making it through to code review. As a supervisor, I’d rather you took some time to setup the basics early, rather than thinking you need to rush into getting a merge request ready as soon as possible.
On the flipside, don’t spend forever trying different setups, different editors, different IDEs. It’s good to have tooling setup that keeps you working efficiently. It’s not good to spend so much time perfecting your vim config and plugin bundles that you never get any work started!
Balancing Business Needs & Engineering Desires
One unfortunate fact of writing code in the real world is that time is finite, and customers need solutions. What this means is that sometimes it’s necessary to find a compromise between engineering perfection and balancing the needs of the business. As engineers, we all take pride in our code, which can often make this reality quite frustrating!
Perhaps an entire module of the code is inefficient, slow, and inelegant. Maybe feature creep has grown what was once a well defined class into something monolithic. These could be refactored and improved - but first you need to consider whether the time investment is worth it. Particularly in smaller companies, there will always be more potential work than there are workers to carry that out, so it’s necessary to be strategic when allocating developer time.
Thankfully, as a new member of staff, you won’t be forced to make these difficult cost-benefit decisions, but you might be disappointed in the decision. It’s important to remember that your supervisor is an engineer too. I want the code to be perfect as much as you do!
An important skill you will learn is taking time where possible to make these engineering improvements, without making a large impact on the timeframe for whatever you’ve been assigned. Perhaps rewriting an entire subsytem is too much, but maybe you could make a positive impact with some small changes made alongside adding that critical feature that sales is asking for. Maybe an area of the code has poor test coverage, but writing a comprehensive test suite is too large a time investment - you may be able to add a subset of tests to cover the low hanging fruit!
Finally, if you think there’s an engineering problem that needs to be solved - try and relate it to how the business operates. The further up the chain of command you go, the less likely it is that that person understands why a function shouldn’t be hundreds of lines long, or why it matters that these API endpoints aren’t well tested, when they can see that customers are using the API without issue. If you can make a case for how these engineering concerns can benefit the business, e.g. by saving developer time in the long term, or by preventing regressions that might impact customers and sales, and then relate this to actual estimates of time saving and cost savings, then you’re much more likely to find that you’re given the green light to make these changes that we’ve all been dreaming of!
Ask Questions, But Think!
Asking questions is yet another aspect of working as an engineer that has a goldilocks zone. A common mistake I see from junior engineers is a tendency to ask for no help at all, which is a huge waste! As your supervisor, I’m there to help answer questions, and there’s no reason to spend a lot of time scratching your head about how a certain component works, or how best to approach a problem, when your supervisor has probably had to think about similar things many times in the past!
As well as your supervisor, make sure you get to know fellow new starters early on. They’ll almost certainly have ran into the same teething issues that you’re faced with, and it’s always good to know that you’re not alone.
On the flipside, it is very easy to ask too many questions, and be too dependant on others. Your supervisor and your other colleagues have their own work to do too, and likely have other people to look after - the last thing they want is to be programming by proxy! You should think about your problems first, and don’t jump to asking immediately; don’t use your supervisor as a rubber duck!
Learn to Read Existing Code
Coming in to work for an established business often involves a huge context switch. At university, you’ll often be working on new code, either from scratch or starting with a small skeleton project with stubs to replace. In the real world, even if you’re implementing a brand new feature, you’ll almost always end up needing to fit that in to an existing system.
You open the repository, and you panic. There are so many files, so many functions, so much state, and a mountain of documentation!
Keep calm, get reading, and don’t give up!
Your supervisor should be able to point you to the relevant parts of code, but you don’t want to rely on them to explain the intricacies of everything it does. Look at the class you’re changing, find where it’s used, read the documentation for its public interface. Not only will you understand how to solve your issue suitably, you’ll also gain some valuable experience to help when you come back to the same code in future. If all goes to plan, you’ll be able to help the next person to make changes - either by sharing your knowledge directly, improving comments and documentation, or refactoring large and hard-to-follow methods into smaller more digestable chunks.
New Technology is Shiny, But Doesn’t Necessarily Help!
This is a very common trope I’ve noticed with new developers in the workplace, who are suddenly faced with working with old versions of software and unpopular programming languages. It’s natural to be annoyed by the perceived limitations of old things, when compared to the exciting new framework that has been doing the rounds on Hacker News. This is especially true where I’ve been working on projects primarily in perl3!
There is overlap with the “business needs vs engineering desires” compromise here. Perhaps Node.js or C#’s asynchronous programming support is much cleaner than the current language being used, but is rewriting a large chunk of code for that worth it?
There’s a few problems with introducing new technologies:
- Existing developers might be lacking expertise in that technology, thus slowing down overall work output
- New versions of software may have a number of unknown bugs or security vulnerabilities, whereas older, stable software has had more time to iron these out
- Interfacing code in a new language with existing code can be a mess - it might seem standlone now, but perhaps later you’ll be forced to reimplement the same functionality as already exists elsewhere
- A large shift to new technologies means that suddenly recruitment needs to change too - interview content, job descriptions, experience requirements will all need to adapt
Keep in mind:
- What may be popular now won’t necessarily stay popular, and it’s likely that current technology choices were influenced by popularity at the time!
- Any language can do pretty much anything that another language can, perhaps with some effort
- Others may have conflicting ideas of what to use - perhaps you want to write microservices in Golang, but others would rather try out Kotlin, and someone else wants Typescript… there’s rarely a single right answer!
There’s definitely a place to introduce new things, but it needs to be carefully considered, as there are always drawbacks, particularly operationally. As always, if you want to introduce something new, you’ll need to satisfy the business demands as well as the engineering requirements, and you’ll need to realise that sometimes you need to take the smaller opportunities rather than trying to introduce sweeping changes!
Conclusion
As you’ve hopefully gathered by now, every bit of advice I’ve given isn’t set in stone. Different teams will have different styles of working, and what works for one team may not work as well for another.
Really, starting out as a software engineer is all about relationship building and communication - writing code is the easy part. Work with your supervisor, and realise that just like you they want to write great software that they can be proud of, but they’ve also gained that hard earned experience of how to keep a team working in the context of the business as a whole. Sometimes there will be frustration, but learning to deal with that and find compromises and opportunities to make incremental improvements will help you grow as a developer, and make you a valuable employee.
As a supervisor, I want my supervisees to be successful - it reflects well on both of us after all! Talk to me, and make sure that you keep your team involved with what you’re doing, but show that you can be independent and can see the bigger picture - you might find yourself in the supervisor shoes sooner than you think!
-
Particularly when you’ve just started! It helps to be patient at the beginning, and take time to learn about the company’s procedures, code style, CI/CD pipeline, and preferred tooling. ↩︎
-
Another particularly difficult example is if your changes take you into an older file that has been left behind as code standards have evolved and improved. I’ve been given code to review where the developer has added a new feature, and has also decided to update the code style across the entire file to meet the current standards. Git’s
--ignore-all-space
flag can make these much cleaner if the changes are largely whitespace concerned, but in more substantial sets of changes these can be very hard to unpick.I’ve found it helps to be strategic in these kind of improvements. If you can improve the style of just the functions you need to change to achieve your goals, then that can be an engineering win without complicating your branches.
Again, not a hard-and-fast rule - it would be silly to discourage efforts to improve code quality, and sensibly splitting your commits can lessen the review headaches! ↩︎
-
An interesting point about Perl5 is that the syntax and the language design is quite archaic, but extremely flexible. While it’s possible to write horrendous perl code, this is more in support of backwards compatibility than a sure-fire thing of new code. It’s perfectly possible to write clean and readable Perl5, just as it’s possible to write unmaintainable code in any other language! ↩︎