Introduction
Our client has a platform for the inspection of fruits and vegetables. This platform consists of a web platform and an app for iPads built with React Native. This case study will focus on the app: the Inspector App.
When we received the project at Sping, it had already gone through a number of other teams. The project had been changed a lot to meet the needs of the users, but the architecture had not grown with the project. This meant a significant amount of technical debt was slowing down the project and preventing users from receiving the features they needed.
The plan
A plan was made to update the project to modern standards. This would make the project more maintainable and allow for new features to be added in the future, such as Android support and supporting phone usage.
Introducing TypeScript
One of the first things we wanted to introduce was TypeScript. This would allow us to catch errors at compile time instead of at runtime. This was a big task as the project did not use consistent types. This allowed us to gain a much deeper understanding of the codebase. We discovered a lot of potential bugs that had not been reported up until this point. Most of these bugs were fixed by validating the types and adding extra null checks. In a lot of cases it also highlighted architectural problems that needed to be addressed, we added comments to highlight these areas and later refactored the code to address them.
Updating the packages
Next on the list was updating the packages to the latest versions. This was important for maintainability. Some of the packages being used had not been updated in 8 years! Some of those packages had gone through a number of major changes which meant that the current documentation did not match what was actually in the code. This problem is manageable for one package at a time but becomes exponentially more complex when you have to coordinate dependencies between multiple packages.
Continuous integration
Adding proper continuous integration to the project was a vital step in enabling the team to move fast with confidence. We chose the following steps:
- Type check with TypeScript
- Lint with ESLint and Prettier
- Run tests with Jest (No unit tests existed at this point)
Automated delivery
When we received the project the app had to be built manually on one of the developer’s Macs and uploaded to the Apple Developer Portal. We wanted to change this as it was slow, labor intensive and error prone.
We chose Fastlane to build and release the app. This would allow us to release the app on both the App Store, TestFlight and the Google Play Store. We ran the build process through GitHub Actions using the workflow_dispatch feature. This allowed us to select the branch and target environment from the GitHub website. Automating this process saved us around an hour per release in the best case scenario. If a new developer had to do this or during the process human error would have occured this would have taken even longer.
Release plan
The real world is messy and technological solutions will only get you so far. Developers can make mistakes, requirements can be miscommunicated or change. We need to validate that what we deliver is what the client needs and have a rollback plan as backup.
Test process
In order to facilitate the proper tests we set up four different environments:
- Integration: for internal testing
- Acceptance: for testing by the client
- Staging: for testing by a select group of customers
- Production: for live users, here we used phased rollout to limit the impact of potential bugs on users
Having the automated build process in place really paid off here. We could quickly release a new build to the different environments and validate that the new version met the requirements.
Rollback plans
While having automated delivery makes it easy to release new versions, rolling back is not as straightforward. Both the Apple App Store and Google Play Store do not allow reverting to previous version numbers. This means that if a critical bug is discovered, we need to release a new version with a fix. To minimize the impact of such situations, we maintained a separate branch for each release, allowing us to quickly prepare and submit fixes based on the last stable version.
Conclusion
This project presented significant challenges in modernizing a legacy codebase while maintaining functionality for existing users. Through careful planning and implementation of modern development practices, we successfully:
- Migrated to TypeScript, improving code quality and catching potential bugs early
- Updated outdated dependencies to their latest versions
- Implemented automated CI/CD pipelines with GitHub Actions and Fastlane
- Established a robust testing and release process across multiple environments
- Created reliable rollback procedures to minimize risk
These improvements have made the codebase more maintainable, reduced development friction, and positioned the project for future enhancements like Android support. Most importantly, the modernized architecture now allows the team to deliver new features to users more quickly and reliably.
The success of this refactoring project demonstrates the value of investing in technical infrastructure and modern development practices, even when working with legacy systems. While such undertakings require significant effort, the long-term benefits to both development teams and end users make them worthwhile investments.
Reflection
Looking back, one area where I would have liked to invest more effort was increasing unit test coverage. The process of writing unit tests, particularly mocking dependencies, often reveals tightly coupled parts of an application that could benefit from refactoring. This insight would have been valuable in identifying additional areas for architectural improvements. Moreover, unit tests allow teams to refactor with confidence. If we had these tests from the beginning we would have been able to refactor the codebase while guaranteeing unchanged functionality. However, given the budgetary constraints of the project, extensive unit testing wasn’t feasible. We had to carefully balance our modernization efforts with immediate business needs and available resources. While we successfully implemented critical infrastructure improvements, this remains an area that could be addressed in future phases of the project when additional budget becomes available.