Breaking down the development of a feature

In the last few years we've been talking a lot at Vinta about team maturity which has lead among other things to the adoption of SRE practices and better observation of DORA metrics in our projects. Simultaneously we've also been discussing topics that I like to put under the "software discipline" umbrella. The way I see it, while things like DORA and SRE act on the overall team maturity, software discipline are practices that improve the maturity of individuals. TDD, refactoring, unblocking, planning, debugging, ... these are all things that are relatively easy to teach but that require an active effort from individuals and a lot of practicing before they become natural and start feeling worthwhile as part of the day-to-day of a developer.

In this article I'm going to share the method we've been preaching at Vinta to help developers to better plan and write features. We've been using this for more than a year now with very good results and feedback from the team. Many senior developers will find that it describes pretty much things that they do automatically, regardless, you might find it useful to periodically use this guide as a checkup of your practices. For everyone else, we recommend following the guide step-by-step reviewing it for for every new feature you work on until repetition fixates the process in your mind and in the way you work.

1. Understand the feature

First and foremost, make sure you understand what you are going to do. Carefully read the whole feature user story, open and read any external documents referenced (RFCs, docs, code snippets, …), inspect images and study the design assets. If it's a change in an existing feature, play a bit with it, test different parameters and flows. If necessary, make questions tagging stakeholders. If it's not possible to wait for answers, try notifying people via chat or scheduling a quick meeting.

If you have multiple features planned for your sprint, run this step for all of them before you even start working on one. By doing this you will gain an overview of your tasks that will allow you to:

  • Have time to gather the missing information in stories (and do this asynchronously without a rush);
  • Prioritize the stories that are ready for development while you wait for more context on others;
  • Better track your pace and communicate delays along the sprint;

2. Understand the code context

Every feature exists in the context of a broader code/project. It's not possible to plan how you are going to implement a feature if you don't understand the parts of the code it will need to interact with. To do this, carefully read the whole flow where that code will be placed at and make sure you understand what is happening in each part. At this point there's no need to dig deeper into implementation details, just read the function/method names to get a broad understanding of the flow. For example, if you are working on a serializer: start from the route, read the controller and the queries, then read all the code in that serializer.

Do not move on to the next steps before you are confident you understand both the feature and the code context;

3. Plan a solution

Now that you are confident about what you are going to do and about the surrounding code, it's time to think about how your solution will look like. At this point we don't want to start writing any code. Just think it through, draw diagrams on paper, map relationships, sketch how things fit together, list all possible scenarios and states, and dig a little deeper into the existing code if you need more context. Once you have an initial idea, check if you are going to use any external libraries. If so, review the library documentation to confirm it has the features you will need and that it behaves as you expect. Double-check if the library is well-maintained, and review the open issues. Also check the library compatibility with your application platform (language version, framework version, license, etc.).

Now it's time to break down the solution in smaller blocks. As you do it, write these down as subtasks of the main task. Think about edge cases, exceptions, integrations, validations. Also, check other functionalities that might be impacted or that will need more testing to confirm they didn't break, then also write these down as subtasks. Finally, consider all the non-functional aspects of the feature: usability, performance, cost, data integrity, reliability, monitoring, serviceability, etc. If you foresee any risk of worsening those aspects in production, ask your Tech Lead (TL) to review the impact with you and devise subtasks to tackle this too.

The last part of this planning is actually one of the most important: prioritizing the order things will be tackled. To do this you need to evaluate the importance and risk of each activity and plan a MVP. What is the minimum amount of work/activities you can do that delivers the most value? What is absolutely crucial to the feature? What is less important and could be left for later? Here are some tips on how to evaluate risk:

  • Is it time consuming? This can either indicate that it needs to move up or down in priority depending on the importance;
  • Can this be a blocker? Again it can either mean it needs to be moved up or down in priority depending on the importance;
  • If a task requires experience with a part of the code or a third party lib that you are not familiar with, it might be a good idea to give it more priority to avoid late blockers;
  • Version zero can often be more forgiving with the interface, unless of course, the UX represents a risk for the feature value. Eg.: a complex animation that you are not sure how to do;

While running this process you will often identify parts of the feature that are too complex or time consuming. When you notice this try to imagine what other similar solutions would make things simpler/faster.

4. Validate your solution

For more complex features or if you are not confident about your solution, it's a good idea to validate your solution with someone else. In most cases a TL will be the best person to review because they have the technical context but sometimes it can be done with a manager, a designer, or some other project stakeholder. Write a paragraph or two explaining how you are planning to do the feature including some but not all technical details and send it for validation. When it's something that cannot be summarized in a couple paragraphs it's probably a good idea to schedule a 10 minutes desk-check to make things easier for everyone.

This is a good moment to report about the complex and time consuming things you noticed. Confirm with stakeholders if the simpler solution you came up with really works. Sometimes it will make sense to completely remove that part of the feature or delay it to another moment. Ensure any useful delayed parts are tracked as new tasks with proper context, for them to be prioritized later.

5. Make it work

Now it's time to start writing code. For now, focus only on the MVP you've defined. It's very important that you don't go beyond it. The goal is to have a working prototype that validates your assumptions. It's important to write the least code possible, we are still learning about how the new feature fits in the existing code and validating the initial architecture we had in mind. Less code means it's easier to experiment other approaches and to change the architecture in case we end up not liking the initial one. But don't go trying architectures until you have a working prototype. In fact, if it makes sense, consider writing this first version as a script completely decoupled from the application and then transplant it back.

Pace your work, don't get ahead of yourself. Because you've planned the execution you can, one by one, pick the topmost subtask you created and work on it individually. Once you've picked an activity, focus on getting that single thing done and forget about any other tasks. Once you are done, write a commit message and mark the subtask as completed, it's important to celebrate progress! Using TDD will make this whole process much easier because it follows the same philosophy of gradual, paced work. It will also help you build a comprehensive test suit that will be essential in the next step (refactoring!).

As you make progress you will identify unmapped edge cases and new requirements. It's important that you don't work on them immediately, simply create a new subtask and prioritize it among the others, this will allow you not to deviate focus, reduce the cognitive load and prevent you from getting anxious.

6. Refactor

With a working first version it's time to work on code quality and prepare the ground for the definitive solution. At this point you should not add any new feature or fix. Try architectures you think might better suit the problem, reorganize interfaces, rename variables, isolate concerns, encapsulate implementation details, and make sure code is comprehensible and clean. Because you've written tests you can be confident the work you've done so far works and that you are not breaking other stuff. Since you only have the bare bones of the feature, refactoring should require minimal effort. Once you are comfortable with the architecture and quality of the code, move on to the next step.

7. Fill in the gaps

Now it's time to close up the feature. Work on the items you judged non critical. Fill in the details, nice-to-haves', polish the interfaces. Keep the paced work, one task at a time, use TDD, and celebrate progress writing commits and marking subtasks as done. Review the code and check if you should add log messages or if some part would benefit from having more code comments.

8. Update and create docs

All good, let's now close the feature up by reviewing docs that need updating and creating new ones. You should also consider writing an ADR to keep track of the changes and to sync your teammates about those.

Filipe Ximenes

Founder & Chief Technology Officer