As I look back to over a decade ago, there are a few things I wish I'd started doing sooner. Habits that could have helped made me grow faster and in a more focused way. This is the advice I'd give my younger self, who has just landed their first professional software engineering job.
1. Take the time to read two books per year on software engineering
Every time I took the time to slowly and thoroughly read a recommended book on software engineering, I leveled up. By properly reading, I mean taking notes, talking chapters through with others, doodle diagrams, trying out, going back, and re-reading.
When I read, I don't do it quickly. In fact, I do it rather slowly. I usually go a chapter or two in one sitting. I take notes or do highlights. When I'm done, I then recap and often discuss with others. I've also started writing book reviews on this site, mostly to reflect on what I've learned. I've picked up these habits the past few years. They helped me grow faster as an engineering manager: and this habit would have served me well as an engineer. Looking for inspiration for books? Here's the list of books I've read and am reading.
Why books over blogs, videos or talk? I'd actually say books on the side of those. Shorter formats tend to skim the surface compared to a book, for any topic. Books are in-depth, and well-organized knowledge. It takes me a few hours to write a post like this, but I've been working close to a year on my book on growing as a software engineer. I think of books as a form of slower, but more in-depth consumption.
Don't be ambitious: one book every six months is already great. Pick a book, and spend the time to properly read it. And after you've read a book or two, I also recommend the book How to Read a Book: The Classic Guide to Intelligent Reading. I'm serious about this recommendation.
2. Learn the language you use at work in-depth, to the very bottom
Two years in, I started to be bothered that I had to rely on senior developers when debugging my C# code. One of the developers who became my goto debugging buddy always seemed to know the ins-and-outs of the depths of the language. He recommended the book C# In Depth for me to study. And study I did. I went all the way to threading, how garbage collection and generics work, behind the scenes. I pushed myself through countless hours to understand covariance, contravariance, and other, difficult to digest topics.
The more languages you know, the more you can evaluate their strengths and weaknesses. By the time I moved over to iOS, I knew a few languages pretty well. When Swift came along, I briefly followed and participated in language discussions, proposing readwrite reflection to be added to the language's roadmap. Knowing the characteristics of the language made it easier to decide what strategy my team should use to migrate from Objective-C to Swift. And the more languages you know, the easier it is to pick up new ones - and go deep easier, when you need to do so.
3. Pair with other developers more often
I feel like pairing is out of style these days. When I started, both Extreme Programming with continuous pairing, TDD, and mob programming were popular things to do. Some of my biggest professional leaps came after pairing with people. These leaps were more significant than reading any book.
A memorable pairing was with a developer who gave harsh code reviews to everyone - including me. One day I had enough, and I decided not to reply on the code review tool, but to sit next to them, asking them to walk me through their comments. I ended up learning a lot - while also telling them I didn't think their comments were fair. They took note and suggested I pair every time I feel this is the case. This is what I did. This developer had a thing for performant code, and through my pairing, I learned about the ins-and-outs of potential performance bottlenecks - and I'd like to think I taught them about maintainability concerns, in return.
Another great pairing memory was doing TDD with another engineer, where we passed the keyboard between writing the test and writing the production code for it. We did this for two days, implementing a tricky part of the system. It's hard to describe how eye-opening this exercise was. We completely changed our approach midway as we made count of all the edge cases. And we also formed a strong bond with this developer, which would last for months after.
4. Write unit tests and run them against a CI
Senior engineers often talk about the importance of unit testing. But the whole thing seems so counter-intuitive. Why would you spend more time writing trivial-looking tests? This was my approach for testing for some time.
In order to appreciate the value of unit testing, you need to have that "aha!" moment, when unit tests you wrote to save the day. However, to get there, you need to grunt away, and write those tests, have them run against a CI. And you often need to do it for months, before you get that "aha!" moment.
For me, I had two of these moments. The first one was when I built the backend engine, as a side project, for a tiny online casino. The API was managing real money, and I was terrified of making mistakes. So I covered all my code with unit tests. The project was late - partially the tests to blame, as they took a lot of time. But it felt right to do it like this. I handed the project over to the customer at the end of the contract. Two years later, they told me how those tests had saved the team multiple times, who were about to push bugs in production - had it not been for tests breaking.
My other "aha" moment was building Skype on the Web. We built a greenfield, Google Hangouts competitor on web.skype.com. The team was a strong one, and we had full unit coverage and heavy integration testing. Three months into the project, an engineer decided we need to refactor the structure of the whole project. It was a very risky refactor, and all of us voted not to do it.
The developer challenged us that given the test coverage, it should be a piece of cake. If the tests pass, it works. I was skeptical. But that's exactly how it worked. After a week-long refactoring, he pushed a giant change... and nothing broke. Not then, not later. All tests passed. This was when I realized the safety net that a strong test suite gives and how it allows refactoring without fear.
5. Make refactoring a habit and master refactoring tools
For many years, I preferred to make the smallest possible change in the codebase, when I was working with a team. For my own, personal projects I did massive refactorings - but I was never comfortable doing this on codebases I did not fully own.
Then I met an engineer at Skype who would continuously do small or large refactors. They all made sense, and the code always got better. And they never messed things up. How did they do it?
As I paired with them, they knew their IDE very well and had refactor extensions added. Extracting a method, renaming a variable, moving into a constant... it took them a second of time.
I realized I was afraid of refactoring as I missed both the practice and the tools to do this well. As I started making refactoring a weekly habit, I got better at both. This habit served me well later on - I just wish it didn't take me so many years to start it.
6. Know that good software engineering is experience. Get lots of it.
When I started out, I used to be intimidated by senior engineers. They saw bugs in my code that I didn't. They knew answers I had no clue about. I assumed they're just smarter and better than me, and accepted this is how things are.
Now that I've worked with many great regarded software engineers closely, and coached several more, I see that there's nothing to be intimidated about. The best software engineers have a mix of learned knowledge and real-world experience. The knowledge you can learn. The experience, you need to go after.
Look for opportunities to work on different stacks, different domains, and challenging projects. It took me seven or eight years to get what I would consider a "senior" level. I see some people joining high-growth places like Uber get there in three or four. The difference? Those people work on challenging projects, pushing just to keep up with the others around them, and often change teams midway, to start again. They volunteer to work on new projects and try out new technologies as the first in the team. I eventually became this person - but not for my first few years.
7. Teach what you learn
The best way to learn something is to teach it. I started doing this accidentally. In 2010, I started presenting on .NET and Windows Phone user groups. My presentations were not good, but I learned lots just by preparing for them.
These days, when I want to learn something well, I sign up to do a public talk about it. I offered to do a presentation on how Uber rolls out backend changes at scale in 2017, a year after joining Uber. When I did, I didn't fully understand how we did this - I was mostly doing mobile development until then, and managing a mobile team. By the talk, I had no choice but learn all the details. And I had plenty of pressure to do so: about a hundred local developers signed up for that event.
Many other people swear on this method working - with Shawn "Swyx" Wang being a prominent figure of the #LearnInPublic approach. His growth story is far more impressive than mine: going from changing careers to senior engineering roles at Netlify and AWS in four years, and writing a book about his learnings. The nice thing about teaching others is you can only win. Not only will you learn something by teaching, but you'll also help and inspire others.
And I don't know any experienced, role model developer who is not a decent teacher and mentor. The earlier you start giving back and teaching, the more natural this will come.
Featured Pragmatic Engineer Jobs
- Founding Engineer, Front End and API at Causal. $175-225K + equity. Boston.
- Senior Software Engineer at Portchain. Coppenhagen or Remote (EU).
- Engineering Team Lead at Portchain. Coppenhagen or Remote (EU).
- Software Engineer - Infrastructure & SRE at Koyeb. €71-105K + equity. Remote (Global).
- Software Engineer - Service Mesh & Networking at Koyeb. €75-105K + equity. Remote (EU).
- Security Lead at Conjecture. £85-210K. London.
- Senior Software Engineer at Consensus. Boston, MA or Remote (US).
- Senior Frontend Engineer at Plain. £75-110K. Remote (Europe / 2 hours within GMT).
- Lead iOS Engineer at Polarsteps. Amsterdam or the Netherlands.
- Distributed Systems Staff Engineer at Reframe Technologies. Remote (Global).
- Senior Software Engineer at Karat Financial. $160-225K. Remote (US time zones).
- Senior Backend Engineer - Ruby at Rise Calendar. €80-120K + equity. Remote (Europe). I'm an investor.
- Senior DevOps Engineer at Droit. $170-205K. New York.
- Senior DevOps Engineer at Droit. London.
- Full Stack Engineer at Applied. £45-69K. London or Remote (UK).
- Solution Engineer at Pigment. $90-180K. New York or Toronto.
- Senior Backend Engineer at Pigment. €60-130K. Paris.
- Senior Engineering Manager at Whimsical. $185K + equity. Remote (US, Canada).
- Engineering Manager at Bobsled. $170-190K. Remote (US, Europe, South America).
The above jobs score at least 10/12 on The Pragmatic Engineer Test. Browse more senior engineer and engineering leadership roles with great engineering cultures, or add your own on The Pragmatic Engineer Job board and apply to join The Pragmatic Engineer Talent Collective.
Want to get interesting opportunities from vetted tech companies? Sign up to The Pragmatic Engineer Talent Collective and get sent great opportunities - similar to the ones below without any obligation. You can be public or anonymous, and I’ll be curating the list of companies and people.
Are you hiring senior+ engineers or engineering managers? Apply to join The Pragmatic Engineer Talent Collective to contact world-class senior and above engineers and engineering managers/directors. Get vetted drops twice a month, from software engineers - full-stack, backend, mobile, frontend, data, ML - and managers currently working at Big Tech, high-growth startups, and places with strong engineering cultures. Apply here.