A list of ten simple things we do to make our projects as maintainable as possible, regardless of the stack.
A while back, I gave a talk at a few meetups about things developers can do to make maintenance as painless as possible. None of the ideas from that talk were groundbreaking, and most are considered best practices. Despite that, when I asked people to raise their hands based on how many of these tools were in use on their current projects, most people were only using around half. Almost everyone had concerns about the maintainability of their projects.
This probably isn't surprising to anyone that's worked in software development. We are never operating in an ideal world where code quality is our only concern. Deadlines, budgets, changing requirements, putting out fires, legacy code, and other pieces of reality force us to make tough calls all the time. That being said, as software developers, we are the ones most responsible for code quality, and part of the job is trying make our codebases better over time. Put simply, maintenance matters.
At Viget, we have a variety of clients with very different requirements, but they all come to us for high quality, maintainable code. One of the things that I love about working here is that we do a lot of things right to make that happen, no matter the size of the project or the stack we're using. While few of these practices will surprise you, taken together, they create the infrastructure that allows us to write good code in a wide array of contexts.
There are many factors that go into a successful project, but in this series, we're focusing on the small things that developers usually have control over. Over the next few months, we'll be expanding on many of these in separate articles, but for now, here are ten things that help us write maintainable software that we're proud of.
1. Required Code Review
Code review isn't just for catching bugs or giving a quick thumbs up. It improves readability, helps keep everyone up to date, and is a great place to learn and discuss things that don't have obvious answers. Multiple sets of eyes mean that we write better code.
2. Default Formatting
While it can be a fun exercise to fight about tabs versus spaces or how to align your hashes, it usually isn't a great use of developer time to bring those preferences into code review and rehash the same arguments again and again. For us, no particular style choice is as important as consistency across a project. To that end, we like to use tools like Standard, Prettier + ESLint, mix format, and others to handle code formatting.
3. Good Tests
There is a lot to say about testing, but from a maintainer's perspective, let's define good tests as tests that prevent regressions. Unit tests should have clear expectations and fail when behavior changes, so a developer can either update the expectations or fix their code. Feature tests should should pass when features work and break when features break.
4. High Test Coverage
Good test coverage is necessary but not sufficient to prevent regressions. With greenfield projects, we like to enforce 100% coverage and explicitly exclude files or code that shouldn't be counted for one reason or another. If we're adding coverage standards to an existing project, we can always check the current coverage, use that as the floor, and increase that minimum as we add tests. Tools like simplecov help us ensure that code changes come in with supporting tests.
5. Enforcement on CI
Once we have standards like the ones discussed above in place, we want them to be enforced using as little developer time as possible. To that end, we use continuous integration tools like GitHub Actions or CircleCI to enforce everything we can. The build should fail if standards aren't met for formatting, the tests aren't passing, or coverage isn't high enough. That way, we can focus on writing good code and giving good reviews, and know that code isn't going into main without meeting basic standards.
6. Timely Upgrades
As we all know, upgrades can only be put off until they can't. It can be hard for someone whose main concern is functionality to prioritize time for invisible changes like keeping dependencies and tool versions updated. But these tasks are important for the health of a codebase, and it often falls to developers to make time for them. Since it is usually easier to manage small version bumps than big ones, we advocate for this time, aim for regular version checks, and use tools like Dependabot when we can.
7. Thoughtful Launches
In addition to writing good working code, it's important to consider how the code will go to production. The right approach might change depending on the stage of the project or size of the feature, but often a little forward thinking can go along way to ensuring smooth launches. Feature flags, phased rollouts, good migrations, and having a rollback plan can make even big deploys feel simple.
8. Helpful Logs
This is probably the least utilized item on this list. A lot of the time, developers don't think much about our logs until we're digging around trying to sort out something that has gone wrong. But good logs can probably save more time than anything else during debugging or maintenance. I am a huge fan of customizing logging behavior as a part of feature work. It takes hardly any time, and can have big payoffs. Removing extraneous information, adding helpful notes, or setting up custom log files for important processes can be the difference between wading through oceans of data and pinpointing the source of a problem quickly.
9. Error Monitoring
Alas, we're not perfect and bugs happen. It is so much nicer to catch our own errors than to wait for a client or user to report them. Tools like Sentry allow us to get notified immediately if something unexpected happens and do a good job of organizing and visualizing issues. If you configure these tools well in your application, you can set yourself up to have most of the information you need to debug and fix an issue sent along with the error before anyone using the application even notices.
10. Solid Documentation
A project with good documentation tends to have pretty good code. That might not always be the case, but thoughtful docs often indicate that developers have been thoughtful in other areas as well. Keep the readme updated. Make sure it includes everything a developer needs to get up to speed, from system requirements to how to work with third party integrations.
And that's everything! Like I said at the start, this list doesn't contain anything new or surprising, but the simple things are important. As our engineering team continues to utilize new languages and platforms, it's been essential to come back to the fundamentals that help us write professional and maintainable code in any context. Keep an eye out for some upcoming articles that will dive deeper into some of these items.