Building Better Software: The Importance of Rigorous Code Reviews

Best practices to help you build a healthy & maintainable code base.

June 11, 2024
and 
June 11, 2024
updated on
June 11, 2024
By 
Guest Contributor

Every engineering team starts a new project with excellent coding standards, great architecture, and the cleanest code they have ever written. New people are added to the team, different coding styles conflict with each other, deadlines approach quickly, and you just need to get the feature in, telling yourself you’ll go back and clean it up later. Later rarely comes, and there is always another deadline or another new feature to build. The codebase is now so large that it’s difficult to grasp how far you’ve deviated from best practices, and you’re not entirely sure what to do in order to get your codebase back to a healthy state.


At Very Good Ventures, our team has done code assessments for small startups, to large enterprise apps, and we want to share some quick tips you can use to evaluate your codebase. To get a sense of how well you and your team are doing in following best practices and standards you’ve set up, check out the 5 tips below.

Documentation

To get started, let’s focus on where new team members look when they start on a project: your README file. Anything that pertains to the code in the associated repository should be here for engineers to get the product up and running quickly and easily. The README should contain things like: 

  • The steps to get the app running locally. 
  • How to run any CI checks locally; like test coverage reports, spell check, and linters. 
  • Code generation commands like those needed to build localization files or serialize JSON. 

Having the relevant documentation so close to the codebase improves the speed in which new engineers being introduced to the project can onboard, reduces time of digging through endless websites and portals for links or commands needed to build a feature, and ultimately allows engineers to selfserve information. We should add here that not all of the documentation needs to live in the README but a good starting point is to make sure everything needed to build, develop, and deploy the code is in there. 

Project Structure

Having a well-organized structure makes navigating the project easier and faster. When structuring your project, it’s best to make sure everything is grouped by domain.  As your project grows, accumulating hundreds of files, broad categorization of these files will result in unwieldy structures inside your code repository.  

Here at VGV, we recommend following a feature-based structure. As the name implies, we organize our source code by the domain feature that a file belongs to. You can see an example below:

Important things to notice is that each feature has its own folder where all related source code lives. Items like the state management structure, views, view components, and their accompanying barrel file.

Your test folder should mirror your lib/ folder. This makes it easy to see which parts of the code have been tested and which have not. The only difference between test/ and lib/ is that each file will have _test.dart appended to it. 

This structure makes it very easy to navigate from one feature to another, as everything a feature needs is encapsulated within its relevant folder. Organizing your code by component (I.E having a screens folder that holds all of your apps screens) eventually becomes unwieldy and difficult to navigate.

Packages vs One Folder

Creating packages for different modules like ApiClients, Repositories, etc, has several advantages over including everything in your source folder. These advantages include maintainability, testability, and CI workflows. You can read more about VGV’s architecture on Marcos’ blog here.

Maintainability: Keeping code up to date is a challenge we continually face in software development, especially when working with parts of software that interact with the outside world like ApiClients and repositories. Having those modules self-contained makes updating and changing them easier than when they are part of the full application. When something like an ApiClient is part of the source code updating that client can have cascading effects across the app. By putting these modules in packages, we don’t expose domain level implementation details. This makes refactoring easier in the future as the implementation of the client doesn’t change, just the details inside the client do, details that are not exposed to any outside uses.

Testability: 100% code coverage through widget & unit tests is a strict standard we follow at VGV. We dive deeper into why we enforce 100% test coverage at VGV in Oscar's blog, but keep reading for a brief overview of the benefits.  Having modules in packages helps in maintaining that standard. When testing parts of the code that rely on a module, you only need to mock the module without needing to be overwhelmed with how you get the information or where the information comes from. Writing tests will help you identify where your coupling is high, and alert you that you might want to consider moving parts of the implementation to a package. 

CI: Having CI workflows implemented is a great tool to enforce code style, automate tests, and other automations. However, it can lead to large execution times as the application expands. Having self-contained packages also helps in this matter, since a change in those can trigger a specific workflow that runs only the required packages, and packages that remain unchanged. You can quickly get these benefits and more by adding Very Good Workflows to your CI/CD.

Testing

When assessing code we always check the code coverage using automated tooling. Our Flutter projects use very_good test --coverage, our Android projects use JUnit, and our web projects use Jest. At VGV, we run this on the main application and all local packages within the project. In Flutter, you'll get a coverage report printout like the one below that I ran on the Google IO_Flip  repository:

This will show you what percentage of each folder/file has a test that covers those lines of code. Covering a line of code is not all that useful on its own. Test coverage helps you verify your widgets are displaying correctly, your processes are outputting the correct data, find and address code smells such as: large complex widgets, and tight coupling of domains as described above in the Packages vs One Folder section.

Code Analysis & Linting

Lastly, we check for strong linting and code analysis. Linting rules are the first line of defense for a healthy codebase. A healthy codebase implies that all developers on the team rigorously follow established rules, therefore we always advise to not use any ignore_for_file or ignore_for_line. These are typically code smells of quick, hasty development and degrade your codebase over time.

At Very Good Ventures we use our open source analysis tool, very_good_analysis. Very good analysis is a lint library heavily inspired by flutter_lints and pedantic (although it is deprecated now), but has a stricter set of lint rules. We believe these linting rules create a healthier, more scalable codebase.

We strongly suggest running very_good_analysis in your pipeline to verify that all code being committed to your code base follows the same rules and hygiene from start to finish. 

Where to get started

Reviewing these five key sections of your product will give you a great starting point for assessing the health of your code base, as well as give you an idea of how aligned your team is, while taking less than an hour to run! 

This is only the beginning though: At Very Good Ventures when we perform a code assessment we do a thorough investigation into the application, where we delve into every detail of the app, from the documentation surrounding it, to the CI/CD pipeline that was used to build and ship it. Afterward, we provide our findings and recommendations for next steps to get it to a better place. 

If you’d like to talk more about how to increase velocity while continuing to build safe, scaleable code, contact us! We are passionate about ensuring code is scalable, while maintaining high quality, and want to help you do it too!

More Stories