Building a financial mobile app at scale with React-Native
I work for a digital bank as Mobile Tech Lead. This article has been written in the context of showing the community what to expect from building a large-scale React-Native application. This is my own perspective and doesn’t disclose the full reality of our mobile application.
When you start building a new application, the possibilities are infinite. You get to choose exactly the technologies you want to use. Starting a project that’s expected to grow to a large codebase is also a huge responsibility. The decisions taken at the beginning of a project can play a critical role for years to come.
How do you choose the right technology, libraries or even architecture that can support the growth, without over-engineering your app from the very beginning? How do you make sure React-Native is the right technology? I will explain what are the important elements you need to consider to scale up your React-Native application with different examples from my company’s app.
The mobile app
The app is designed to help customers access their financial products on the go, without the need of using the website. Customers can check their balance, make payments, or even apply for new products directly from the app. As of today, the app is used by over 200,000 unique monthly customers and is rated ⭐️ 4.7+ on both App Store and Google Play store.
When I joined the company, the front-end was shifting from a Ruby-based web app to micro-frontend React applications. The company naturally decided to use React-Native for the mobile application.
When React-Native is picked to build a mobile app, it’s usually for a need to grow fast with limited resources. The technology allows to build multiplatform apps, enables developers to reuse code with the web, and above all, the option for the company to hire developers who can work on both the web and the mobile app without too much of a learning curve.
The choice of technologies
We’ve started the initial development of the application when I joined the company about 4 years ago. A prototype was initially created to test the capabilities of the technology.
We’ve decided on a few libraries and technology tools around React-Native from what we thought would have the best future for a long-living app. The adoption of Typescript was one of the first decisions we took. At that time, the language wasn’t yet as spread as today but was taking some market shares to Flow types. A risky decision that paid up.
Deciding on the app architecture
As the company grew and the business needs did evolve, we’ve added several features and more products to the mobile app. This also meant for the developers they needed to add and modify code along without breaking anything.
Unlike web projects that can be broken down into smaller applications and deployed independently, a mobile app requires building and deploying everything into a single binary to the different stores. Unless you decide on building an internal strategy to separate your codebase into completely independent modules, it’s likely all the codebase of the application lives within a single monolith repository.
One way to avoid poor-planning headaches is to structure your codebase around the concept of modules, each with its own responsibility. We needed to adopt a front-end architecture that can grow and scale up easily to avoid multiplying big balls of mud where everything depends on everything. This could easily happen when the overall architecture of the app does not have enough constraints, in particular when developers are free to decide their own architecture when building new features. You can usually recognize a good architecture when moving a piece of functionality takes very low effort.
If you’re interested in learning more about resilient front-end architecture, I definitely recommend this talk from Monica Lent: Building Resilient Frontend Architecture
In our case, we’ve rapidly decided on using a modular folder structure for our application, where we separate our code logic into smaller, single-purpose business functions. This means we’ve created constraints on what a module can do, can use, and can provide to the other parts of the application to limit the interdependencies and risks that come with coupling features together. As the application grows, we add additional modules or break down existing ones into smaller modules.
The application modules are categorized into the following types:
- Core: The core holds all the root logic that isn’t related to a product, such as managing access to the app. This is also where all the app features and libraries get initialized.
- Data: The data is where we provide API definition and store data state used across the app. Data packages can be consumed by all other module types. They can import other data packages as well as long as it doesn’t create circular dependencies.
- Products: The products are isolated pieces of functionality that are used by our customers, such as the customer profile. Products cannot import code from another product. The communication between products only occurs with data or shared modules.
- Shared: The shared modules contain code that can be imported across the app. Those provide pieces of functionality, reusable components or states, used in different parts of the app. They can import other libraries as long as it doesn’t create circular dependencies.
With such an architecture, we have uni-directional dependencies between the features and the core, while the shared and data modules allow us to share the code we need across different parts of our application.
I’ve written an article a few years ago about modular architecture for React applications. If you are interested in the concept, you can check the article:
Why React developers should modularize their applications?
The modular structure concept applied to your React applications
Maintaining a healthy codebase
We aim to update our third-party dependencies regularly. It is a time-consuming job, but most importantly, it’s a way for our codebase to stay healthy and ensure it doesn’t deteriorate over time by keeping older approaches and paradigms. The library updates often provide performance improvements and bug fixes that could make a big difference for the app but also the velocity of the team. We usually don’t update immediately when new major releases are out — we all know how buggy major releases can be — but we try not to be more than 1 or 2 major releases away for all our libraries. Jumping too many major releases can be challenging as the number of changes is usually massive, especially when you update React-Native itself.
Updating our dependencies forces us to go through the entire codebase regularly to make appropriate adjustments. With libraries getting deprecated and code that might just be leftover from removed features, we always find something to clean up and improve our codebase. We want to ensure our codebase does not slowly deteriorate over time which could happen while some features are left out without touching them for months or years. Some of you might think it’s a terrible idea to touch code that already works. But for us, it’s more about evolving the existing code rather than changing the actual business logic. It’s about rewriting code using the latest tools and prevent obsolete code which could break existing features.
To help the team find time to keep our codebase healthy, we also have what we call “Playtime” on Friday afternoons. We endorse mobile developers from all product teams to contribute to improving the application. Developers have the freedom to pick some engineering work they consider would be beneficial to enhance our mobile app. They could spend time experimenting with new libraries, cleaning up some code that is not used anymore or perhaps pair program with other developers to find solutions to complex problems. Prevention is better than cure.
Delivering with confidence
One of the biggest challenges is to deliver on time while ensuring a high level of quality. In order to achieve this, it’s essential to have tools in place to analyze, automate testing and deployment and avoid human errors. This can be achieved with various linter and libraries controlling the quality of the application on a regular basis.
Most of our controls happen during a pull request. Once a pull request is opened, we have a linter running, unit and component tests, integration tests, but also codebase analysis to ensure there is enough test coverage for the introduced change. It is not possible to merge a code change without those checks passing. We also have other engineers reviewing the code to ensure code validity.
The mobile app is released on a regular schedule that we call “Release train”. If a new feature is finished on time, it will be released with the next release train. If it is not ready before the train departure, the train will be missed and the feature will not be released until the next train. All the teams contributing have to follow the release train. The releases are coordinated by a mobile engineer taking the role of the release manager. This role changes for each release to allow us to share the responsibility and get feedback to improve the existing process.
A few days before the train’s departure, we freeze the code (we create a release branch) and developers are not allowed to add new features to the codebase. From there, we start a set of tests and quality control to ensure all the features are working as expected. Most of them are automated, but we also manually test some features. When bugs are found, we fix them directly on the release branch and ensure all the tests are all green again. We also have the input of some employees using beta versions of the app helping us catch some nasty unexpected bugs. Once all the teams are happy with the release, the train departs, the app is pushed and deployed to production.
Once in production, we use different tools in order to control the adoption and stability of the app. We receive alerts when something doesn’t go as expected.
We also have a set of “feature flags” allowing the teams to decide which features our customers should see when they use the application. The feature flags can be updated remotely, so we can turn on and off features without releasing a new binary to the stores. This allows us to A/B test some features. But most importantly, if an issue exists in production, we have the possibility to temporarily disable a feature, which can prevent customers from facing issues.
React-Native has great possibilities. It’s a fantastic framework that can help anyone build pretty much all the mobile applications possible, even banking apps. Yes, there will be some friction from time to time. And you will probably also need to bridge some features natively for your application. But at the end of the day, the added value of the framework compensates for the challenges faced.
More articles from me
- Why React developers should modularize their applications?
- Level up your career as a React Developer
- What are the main differences between ReactJS and React-Native?
Hi, I’m Alexis! I’m a Mobile Engineer focused on supervising and building applications with React-Native and Typescript. I’m specializing in solution architecture for large-scale applications and I enjoy spending countless hours learning and playing with new technologies to use in my next projects.
You can follow me on Twitter: @alexmngn