July 31, 2017 – Howard Wilson – 8-minute read
As developers, we sometimes find ourselves faced with feature requests that will take weeks of work and touch many areas of the codebase. This comes with increased risk in terms of how we spend our time and whether things break when we come to release.
Examples might be moving from a single to multi-tennant application (scoping everything by accounts), or supporting multiple currencies or time zones. This post brings together some tips that we find useful at Drivy for approaching these types of problems.
In general, our goals are to build the right thing, take the right implementation approach, and not to break anything. We’d like to try to do those things pretty quickly, too!
When working on large features, the cost of building the wrong thing is higher than usual, since there is more time between receiving a feature request and presenting the completed feature back to the product team for validation. This makes up-front communication a very important part of the process, especially since more agile startups often don’t use formal specification documents.
One way to avoid misunderstandings is to make a list of assumptions. Check these with the product team and include them in pull requests so any reviewers are aware of them (and can challenge them). Assumptions might take the following form:
It’s always worth questioning whether a large feature really does need to be released all in one go. Are there smaller pieces which all add value incrementally? For example, Drivy now supports precise times for rental start and end, instead of just AM/PM time slots. But we didn’t need to make this change all in one go. We started with bookings, then moved on to booking changes, and eventually the state changes of the rentals themselves.
There are normally several ways to solve a problem. Taking the right implementation approach is the “tech side” of building the right thing. In other words, “will we solve this problem in a way that the tech team generally agrees is appropriate?”
Often, naming is a useful place to start. Getting a few developers together to talk about what any new entities or concepts will be called can help to identify the right abstractions in our code. Even if sometimes they feel like isolated implementation details, the abstractions developers select can strongly influence terminology and understanding across other teams in the company. For example, modeling an
Order rather than a
Request can have a profound impact on the perceived urgency of that user action.
Are data flows or processes changing? Even if not explicitly designing a state machine, it’s exactly these kinds of problems that typically take a lot longer to discuss and get right, than to implement. There’s nothing wrong with taking time up front to properly explore the different possibilities. Draw on the whiteboard and get the opinions of the rest of your team!
The purpose of a spike is to gain the knowledge necessary to reduce the risk of a technical approach. It also often gives developers enough confidence to more accurately estimate how long the feature will take to develop.
Some general guidelines:
Reviewing code is hard. A reviewer is expected to make sure the code is correct and of a high quality before it gets merged into the release branch. In order to do this effectively, it’s usually best to keep the size of the pull request to a minimum. But how then will a reviewer be able to get an end-to-end perspective of your implementation?
One answer is to split code reviews which validate a general approach from code reviews which accept code into production. Here, we’re doing the first type of review. It’s a review best suited to a senior member of the team; ideally someone who has a broad knowledge of the codebase.
Here’s what we like to do:
Again, the goal at this point is to validate the approach, but without losing sight of how we’ll structure the feature for release. This code isn’t going to be merged into the release branch in it’s current form.
This takes a little more time if the pull request has already been split into separate commits, but git helps us re-write our branch. There’s plenty of information on how to go about this in the related post: “Editing your git history with rebase for cleaner pull requests”.
Of course, there are lots of visual tools too (such as Gitup), which get the same job done without using the command line.
Once the branch is updated, we force push back to the same remote branch to preserve a clean commit history.
Before starting to release any code, it can be worth verifying that things expected to be true in production are actually true. Let’s say our new feature is going to introduce behavior which depends on the
country of active cars. We can check in the database to ensure that the expectation “an active car always has a country” is true, but that doesn’t give 100% confidence. It may be true a few seconds after activation, but not immediately.
What we can do in cases like this is introduce some logging where our feature will go:
Once we have more confidence in this precondition, the logging can be replaced with a guard clause which raises an exception. Not only does this mean that we can be confident in our assumptions in production, but other developers will also understand immediately which preconditions are satisfied and benefit from the same confidence when coding.
Now it’s time to get the feature into production. Typically, having split the original pull request, each commit can be
git cherry-pick‘d to a new branch in turn and then improved to a production-ready state:
This time, Github’s suggested reviewers facility is a good way to find someone on your team to review the code. The suggestions are based on git blame data, so they’ll be people who are familiar with the code being changed. Their goal is to confirm that the changes are safe to release. This should be a much quicker process, since the quantity of code is smaller, and the overall approach has already been agreed.
We often write small jobs to test the long term “postconditions” of a feature. Or in other words, ensuring ongoing data consistency. Usually this concerns aggregate data that is difficult to verify synchronously. For example, checker jobs might verify that there are no overlapping rentals for the same car, or that there are no gaps in the numbering of our tax documents.
These jobs usually send an email to the appropriate team if inconsistencies are detected. They’re cheap to create, and can help to catch unexpected outcomes before they become too problematic.
Remember, this is just a selection of ideas that we find work well for us. Your mileage may vary!