Clear code is good code.
Optimise for clarity and understandability above all other criteria. As developers we spend more time reading than writing code. The single biggest impediment to velocity (and a leading cause of software bugs) is code that is not easily understood.
- Is easier to reason about when bugs occur
- Is easier to modify when requirements change
- Is easier to hand-over when developers leave (or new team members join).
It takes time, experience and motivation to solve complicated problems in simple ways. Make the investment upfront to simplify (even if it involves writing more code) and reap the rewards over the long-term.
Pick boring standards, stick to them.
The specific coding standards you use on a codebase is not important. What is important is that you pick standards and strictly adhere to them. This means standards for spaces vs tabs, indentation size, variable casing (camel, snake, etc.) and code formatting should all be clearly defined and enforced (ideally automatically via linting tools).
Go with popular industry standards and avoid bike-shedding at all costs. Building software systems is complicated enough — don’t invent reasons to complicate it further.
Manage technical debt.
Technical debt is a lot like financial debt. A little bit is not only OK, but is actually a good thing as it allows you to “bring the future into the present”, or in the specific case of software: deliver features and functionality to users more quickly.
However, like financial debt, technical debt needs to be consistently serviced (paid down) otherwise it will get out of control. Allocate some hours each week to refactoring problematic code with a focus on code that is particularly risky and/or will be frequently modified by your team.
Leave things better than you found them.
We should always strive to improve the quality of our software. Just because a previous developer cut corners or implemented a hacky solution is not a justification for us making the problem worse by doing the same thing.
If we’re fixing a bug or adding a feature to an existing system, we should always aim to leave the code in a better state than when we found it.
Automated tests are table stakes for any company wanting to deliver software at scale with reliability and speed. With that said, writing tests takes precious development time, so an all-or-nothing approach is not realistic.
Instead, you should write tests pragmatically, focusing on situations where they provide the most value. Test deeply rather than widely, with a focus on integration and end-to-end tests. Write unit tests for particularly complicated or risky classes, but not for every line of code you write.
Use code comments to explain WHY, not HOW.
Code comments should be used minimally. This is because the code you write should be so simple and clear that it should be self documenting. If you need to write a comment to explain how something works, you should treat it as a code smell and try to refactor the code to make it simpler.
Sometimes code comments are required, but these necessary comments are to explain why code is a certain way rather than how the code works. These comments usually describe corner cases or unusual business requirements that mean the code is non-intuitive. For example: when a API implements specific (and perhaps strange or unexpected) behaviour to satisfy a legacy client.
Be wary of third-party dependencies
Adding a third-party dependency inherently links the security and maintainability of your application to that dependency. You not only open a new vector for bugs and security holes, but also commit your team to maintaining a dependency tree that satisfies the third-party library.
Before adding a library, think about whether it might be easier to just rewrite the functionality, or copy and paste a subset of the library directly into your repository. If this isn’t possible, make sure the library you’re importing is reliable and well-supported (check the number of GitHub followers, active downloads, issues/bug reports, etc), and that you have a maintenance plan in place to support the library (and any future upgrades).
Pull requests that result in the inclusion of a new dependency should always justify the decision.
We have limited resources, so we need to focus our time and efforts on actions with leverage and/or compounding effects.
Repeated manual processes should be automated (where possible).
Bug fixes should resolve root causes rather than surface-level symptoms.
Pay for solutions that save development hours, improve customer experience and/or reduce downtime with demonstrably positive ROI.
Work from first principles.
Don’t jump on bandwagons. Software development is hard, but there are countless people who have financial incentives to convince you otherwise, if you just buy their MagicBullet™. Don’t fall for it.
Think and make technical decisions from first principles. Gather quantitative feedback and use it to support your decision making.
Make a commitment to quality.
Commit to doing your best today with the skills and knowledge you currently possess, while also acknowledging that software development is a craft that needs to be honed over a lifetime.
Constantly work towards mastery. Look for better ways of doing things and be open to new ideas/approaches.