Prioritizing Maintainability over Fast Development
Startups often distinguish themselves from larger companies by how fast they move. It’s easy to criticize larger organizations for moving slowly because they’re dependent on bureaucratic decision-making processes. Young startups have the luxury of nimbleness, but often times their speed comes at the cost of the maintainability and quality of their products, which will suffer when the time comes to scale.
At Argo, we generally prioritize maintainability over fast development and performance. When beginning any project, it makes sense to optimize for speed because priorities are changing quickly — we want to be able to test our ideas before committing extensive engineering resources towards their execution. As we grow and start depending more on our own infrastructure, we strive to avoid excessive technical debt — poor design decisions that make it harder to build in the future. Technical debt compounds and sometimes can cause more long-term damage than the short-term delay of a particular feature.
We avoid building up technical debt by building habits for writing maintainable code at an appropriately fast pace. This means code that lays a foundation for future development and quickly validates itself in solving the problems at hand.
Writing code people understand
The first step to writing maintainable code is to develop engineering habits that make your code easier to work with. Early on, we introduced rigorous code-review processes to our team. These processes set a precedent for acceptable coding practices. Tools like Phabricator and Go’s development tools help us enforce those practices. I’ve found that internal tooling can have a non-trivial impact on the habits an engineering team forms by doing simple things like reminding us when we’re doing something wrong. For example, Go’s gofmt
tool makes it difficult to write inconsistently-styled code because it formats it for you. Go's linter will yell at you if you don't use a variable or forget a comment. These tools help us form habits from the beginning that ultimately produce consistently more maintainable code.
Technical debt compounds and sometimes can cause more long-term damage than the short-term delay of a particular feature.
Implementing a code review process with Phabricator made a huge impact on how we communicate and think about our code. Our engineers scrutinize every revision to evaluate it for conceptual clarity. This usually entails critiquing simple things like variable names, sensible code organization, and missing documentation as well as more complex strategy and implementation concerns. Because engineers want to get their work through the code review process quickly, they’re incentivized to educate their peers regarding how it works. This also forces us to constantly reflect on how we’re doing things, as opposed to focusing exclusively on what we’re doing. The impact is that over time, the whole team better understands the codebase and we’re able to find problems before they grow too large.
Writing code to last (but not at first)
Writing maintainable code requires a balance of validation and planning. I like to adhere to the policy of “if we’re going to do something, we’re going to do it right,” but sometimes we don’t know what is right. One solution I’ve found is to tackle projects in small iterations, taking each to completion. When we start a project, we quickly develop a usable prototype that we can use to validate our assumptions, and plan infrastructural improvements (and sometimes complete re-writes) for when we better understand our needs. Or, our needs evolve. In practice, we usually spend about half our time on infrastructure and half our time on feature development. This is similar to Amit Kumar’s half-life rule for startup code longevity — -develop code with an intended lifespan comparable to the age of your startup — -only applied at the project level. This is useful because projects sometimes mature on their own timeline. Validating a project in the short term while planning for future revision has helped us prioritize maintainability in the long run, but not at the expense of an iterative design process.
Think of each piece of code as a small product in itself, where the engineering team is the customer.
When validating the usefulness of an infrastructure project, I like to think of each piece of code as a small product in itself, where the engineering team is the customer. This mentality, combined with a modular system architecture, has made it easier for us to plan long-term infrastructural development and validate whether our projects are moving in the right direction. Thinking about each modular project as a product causes us to think ahead more about the role of each project in the codebase. If we don’t feel comfortable reusing our infrastructure in a different product with similar needs, it probably isn’t cleanly designed, and it probably isn’t very maintainable.
Ruthless prioritization requires striking a balance between competing core development principles. That balance is largely built on top of habits formed early on. Startups have uniquely high agility; they should leverage that advantage to always have a healthy balance between maintainability and speed when writing code. If you miss the opportunity to form critical habits for maintainable code, then you won’t know what’s broken until it already hurts.
Originally published at argo.io on January 12, 2015.