- We instinctively equate programming with writing code. Because of it, we focus primarily on code design and creation skills: mastering languages and frameworks, clean code, design patterns, TDD, architecture.
- But creating new code is only a fraction of what we do every day. We spend much more time navigating, searching, analyzing, reviewing, and debugging code than writing.
- Though these activities seem straightforward, they require as specialized skills and experience as coding. Mastering them is what separates true senior programmers from the crowd. And what makes you stand out and get noticed at a new job.
- In this post, I'll look at the key traits of professional software development, how they impact what you do every day, and explore what skills you need to become a complete programmer.
People instinctively equate programming with writing code. And it doesn't apply only to lay people. Many experienced programmers also do have this bias. Not in a naive way, of course. We know that coding isn't just typing and that having crazy keyboard skills doesn't make you a great programmer. But even then, we focus mainly on code creation skills.
Think for a moment what most books, blog posts, and talks are about. What sparks the most heated discussions in your team. What are the most common coding interview questions.
Clean code. Deep, almost arcane knowledge of programming languages and frameworks. Design patterns. Architecture. Algorithms and data structures. Writing beautiful unit tests.
Those are fundamental skills, yes. But they still focus mostly on code creation.
I'm not saying these skills aren't important. They are critical for a senior developer. And there's so much depth there that they can be studied for years. But they cover only a small fraction of what we do every day. And focusing only on code creation skills, while neglecting the rest, will make you a very incomplete and ineffective programmer.
To call yourself a true senior programmer, you need a well-rounded skill set. But to develop this skill set, you first need to acknowledge what actually defines how we work.
Professional software development has a few traits, which have a profound impact on what we do every day as programmers:
- Software development is a team activity and most of the software is long-living. What this means in practice is that the majority of the code you'll work with was either written by someone else, modified by someone else since you wrote it, or written by you sufficiently long ago to forget it. It also means that most of the code you'll work with will be legacy.
- Humans are prone to mistakes. No matter how experienced you are, how rigorous is your process, and how clean and modular is your codebase, you'll still introduce errors. And a lot of them. They may not (hopefully) land on production if your Quality Assurance process is tight, but during local development, you'll have to constantly cope with bugs.
- Modern software stacks are too complex to learn everything by heart. The number of technologies, the surface area of all the APIs, and the pace of updates are simply too high. Even if you are a seasoned programmer, working for years with one stack, it's impossible to just continuously write code without pausing to look stuff up.
- Commercial software is built under tight time and resource constraints. Unless it's your personal hobby project, you'll be constantly struggling to balance cutting corners (to generate business value faster) and keeping the codebase healthy (to not cannibalize your long-term performance).
- Understanding a new codebase quickly is a critical career skill. In a new job, the first impression you make sets up how you'll be seen for a long time (if not forever). And it depends on how fast and self-sufficiently you can find your way around the new codebase. If you are able to start meaningfully contributing right off the bat without much guidance, you'll be immediately perceived as a strong senior.
When you look at the above 5 traits, you can clearly see that your most important skills are the ones related to reading, analyzing, and reviewing code, navigating unknown code bases, debugging, finding information, and being able to walk a thin line between under- and over-engineering.
But let's explore these skills more in-depth.
Mapping the lay of the land. "Reading" the overall layout of the codebase (directory structure, architectural layers, main business domains) and building a mental model of the flow of data and logic through the application. Knowing how to efficiently map, diagram, and visualize the code, data, and flow.
Navigating and searching the code. Using the full power of your IDE to efficiently find your way around the codebase: navigate up and down component or inheritance hierarchy; navigate up and down function or method call chain; look up the definition of the function, class, component, or constant; look up the dependencies of a class, function, or component; find all the occurrences of a class, function, or variable; quickly switch between code and related tests; quickly find component, class, function, or constant by name. BONUS: knowing how to structure the codebase and name stuff to make it easily navigable.
Exploring the Git history. Effectively navigating through the commit and pull request history to understand the context and the decisions behind the current state of the code. Using tools like git blame to identify where to find the necessary information. BONUS: writing good commit and pull request descriptions that will make exploring Git history easier for other developers.
Exploratory testing and code inspection. Using manual and automated end-to-end exploratory tests to see how the application works (which is often hard to understand from the source code alone). Stubbing API requests with fake data to get the application into the desired state to explore all the edge cases. Peeking into the application's inner workings by the means of characterization unit tests, debuggers, and inspection tools (web browser dev tools, interactive shells, request capturing tools like Postman, etc.).
Understanding the overall debugging principles and approaches. Top-down vs bottom-up debugging. Divide and conquer approach. Tradeoffs of using different approaches to pinpoint the problem (for example, debugger vs "print" statement). What input data to use to most efficiently replicate the bug, and at what level to insert this data: UI, web requests, or somewhere inside the code.
Debugging and inspecting the code. Deep knowledge of the whole range of debugging tools and approaches: integrated and external debuggers, "print" statement and equivalents like browser's console.log, in-code assertions, using unit tests to debug the code, using git bisect. Deep knowledge of the tools to inspect and interact with the running application: browser dev tools, your framework dev tools (for example, React or Redux dev tools), interactive shells, tools like Postman to capture and inspect API requests, and so on.
Interpreting and following error messages. Knowing the most common error messages of your language and framework. Understanding how they are formatted and what additional info they contain. Tracing error messages to the actual place in the source code (knowing how to interpret stack traces, use tools like source maps, and so on). Looking up less common error messages.
Monitoring and logging. Using server and cloud platforms' logs to detect and triage errors. Using log management and visualization tools like Grafana, Kibana, or Splunk to pinpoint errors easier. Utilizing monitoring tools like Sentry, AppSignal, or NewRelic.
Understanding the context. Discerning what the original code does and what the change was intended to do. Building a mental map of what exactly was changed and how it impacts the workings of the app. Using tools like your IDE, Git command line, and Git UIs to effectively navigate code and diffs to understand the changes and their context.
Understanding what to focus on. Which changes are critical and which ones are cosmetic. Which will impact code security, architecture, readability, and long-term maintainability, and which ones are more a matter of personal taste (and should be left to the pull request author). What requires human analysis and what can be left for linters and code formatters.
Efficiently reviewing (and re-reviewing) the code. Efficiently navigating through the changes and the discussion (other reviewers' suggestions, pull request author's replies). Understanding how to most effectively review a PR depending on its size, complexity, and quality of individual commits (if it should be reviewed commit-by-commit or all the changes altogether, etc.). Utilizing more advanced features of your code hosting platform like GitHub's multi-line comments or suggested changes. Effectively re-reviewing fixes applied by the code author to your (and other reviewers') suggestions.
Collaborating with the author and other reviewers. Writing constructive, non-aggressive, easy-to-understand suggestions, that are helpful to the pull request author and the rest of the team. Clearly distinguishing the "must-fix" suggestions from the "for-the-purpose-of-the-discussion" ones. Knowing when to press and when to step back - and when and how to gracefully end the discussion.
BONUS: Making your pull requests easy to review. Writing great pull request and commit descriptions. Knowing how to make a pull request easier to review: when to do one bigger commit vs multiple smaller ones, what should be the pull request boundaries, and so on.
Having a good understanding of the documentation for your stack of choice (how is it organized and how to most efficiently find what you need). Getting good at looking up stuff on the internet (Google, StackOverflow, etc.). Using the full power of your IDE: inline documentation, parameter hints, go to definition, and so on.
Understanding technical debt. Understanding the tradeoffs between under- and over-engineering: which debt is worth paying off right now, later, and never; which should be paid off gradually and which as a one-off, bigger project. Setting boundaries of what should be paid off in the current batch. Understanding how to work during the transition period when paying tech debt gradually.
Isolating legacy code and covering it with a safety net. Identifying natural boundaries ("seams") to isolate areas of legacy code for controlled, limited changes. Covering the isolated area with the temporary characterization tests at the seams, to provide a safety net against the regression during refactoring. Converting characterization tests to the proper focused unit tests after refactoring.
Restructuring and refactoring the old code. Knowledge of refactoring patterns and techniques (especially extracting and modularizing the code to contain the scope of the refactoring). Using your IDE, codemods, linter auto-fix, and similar automated refactoring tools.
If you want to advance your career, you have to be a complete package. Code archaeology, debugging, reviewing code, finding information, and working with legacy code, are absolutely critical skills to have (especially as a complete bundle, as they interact and reinforce each other).
Not only you'll spend more time on these activities than on the actual code creation - but they also enhance your creation skills. And they are what screams "true senior" to your colleagues and managers.
💯 Are you a COMPLETE senior dev? 💯 Do you have all the skills to catapult your career to the next level? ➡️ CHECK MY ULTIMATE GUIDE TO FIND OUT. ⬅️