Principles and guidelines for (good) software development
How do I write good software?
This is a question that all software developers contend with throughout their careers, the answers changing as our experiences grow and expand. This post, which I’m hoping to update from time to time, summarizes my own answer to this question and the principles, experiences and thoughts that inform it.
What is good software?
It’s hard to answer the question of how to write good software without answering another, more fundamental question: What makes software good?
In turn, this question raises another, even more fundamental question: Why make software at all? What is the goal of software development? Let’s start from this one.
The goal of software development is to solve problems through software. More specifically, the goal of software development it is to develop and maintain software instruments that assist their users in overcoming the hurdles and challenges scattered throughout their lives.
Software, therefore, can only be as good as the degree to which it helps its users confront the problem that the developer set out to address.
However, software is never only used. It is used, obviously, but it is also engineered, written, read, operated, marketed, sold, maintained and ultimately deprecated. In most cases, these actions are carried out by different people with different backgrounds and different objectives, often in conflict with one another.
The tension that arises from the contrasting interests that manifest around software is the primary impetus behind a number of practical dynamics that characterize it:
- Software is read significantly more than it is written
- Software is executed significantly more than it is read
- Software builds upon other components and processes and is built upon by other components and processes, often with lifecycles wildly different from that of software itsef
- Software is often relied upon well beyond the expected end of its lifecycle
- Software is often deployed in production before it is considered ready
Software, therefore, can only be as good as the degree to which it fits these dynamics, with the intimate understanding of their profound humanity and of the fact that software is ultimately built for humans.
Note that good software doesn’t necessarily coincide with useful software. These concepts are orthogonal to each other, the former being intrinsic to software itself and the latter depending on the specific issues that a user is facing.
Coalescing the above into a set of memorable, easily defined properties, software is good when it is:
- Inspectable: easy to understand and troubleshoot, explicit and straightfoward about what it does and how it does it, immediately approachable by developers unfamiliar with it
- Maintainable: easy to adapt and evolve within its functional boundaries and technical foundations
- Operable: easy to deploy and configure, informative of its internal state
- Reliable: correct, consistent, predictable, safe, secure
- Usable: accessible, intuitive, responsive, communicative
- Performant: fast, light on resources
- Respectful: an ethical and transparent steward of its users’s data, interoperable with third-party software that augments, complements or competes with it
Guidelines for developing good software
What follows is a set of practical and actionable guidelines that, based on my experience so far, significantly increases the chances of a software development effort to produce good software as per the above definitions and characteristics.
Coding
- Prefer greater degrees of type-safety
- Prefer stateless to stateful
- Prefer explicitness to implicitness
- Do not encapsulate state directly related to business logic
- Prefer a better debugging experience over conciseness
- Prefer return values over exceptions
- Prefer crashing the process over handling unexpected exceptions
- Prefer integration tests to unit tests but strive to have both
- Keep logs intentional, tidy and informative
- Decouple a project from its dependencies by wrapping appropriately
- Minimize the time and resources required for building / compilation
Engineering, Architecture, DevOps
- Start small, ship often and regularly (at least internally)
- MVPs, prototypes and demonstrators should compromise on scope, not on quality
- Prefer boring technology over passing trends
- Microservices are not inherently better than monoliths
- Prefer reproducible builds
- Prefer local-first CI/CD
- Do not surrender to cloud vendor lock-in
- Prefer open standards, particularly for interfaces, particularly for public interfaces
- Versioning, CI/CD, issue tracking, security assessment and project management tools and methodologies are only as useful as the difference they make in how good software is
- Prefer simpler tools and methodologies over complex or complicated ones
Dependencies
- Minimize number and size of dependencies
- Code vendoring is not evil when used judiciously, particularly for very small dependencies
- Use dependencies for efficiency, not ignorance; read your dependencies and understand them
Documentation
- Maintain technical, functional and operational documentation together with source code
- Distribute technical, functional and operational documentation with build artifacts and compiled products
- Write documentation using simple, portable, versionable formats
Working with others
- Hold yourself and your colleagues to reasonable expectations
- Be kind and forgiving when someone makes a mistake, be it you or someone else
- Catastrophic failures are never due to single individuals but to poor (or absent) internal processes
Project management
- Feature work should begin with a limited number of iterations upon a
one-page document detailing:
- what the feature consists of
- why the feature is needed (business case)
- who the feature is for (target audience)
- who is going to build it
- how it should be built (from a technical standpoint)
- how it should be evaluated
- when it is going to be ready
- when it is going to be worked on
- Feature work should never begin in the same week it is laid out
- Feature work should be evaluated against quantitative data / metrics
- Most features will prove to be useless in the long term
Additional reading
My approach to software and software development is informed by that of countless others that regularly spend some of their time discussing and writing about these topics. In conclusion to this post, it feels only appropriate to highlight some of their wonderful writing:
- Dan McKinley’s Boring Technology Manifesto
- Adam Johnson’s elaboration on Mike Acton’s “Expectations of Professional Software Engineers”
- Ruben Verborgh’s Programming is an Art
- Fernando Borretti’s specification of the Austral programming language
- Tom MacWright’s writings and work
- The Small Techonology Foundation’s definition of Small Technology
- Benji Weber’s Why I Strive to be a 0.1x Engineer
- Uselessdev’s You will never fix it later
- Nikita Prokpov’s Software disenchantment