When talking with clients, we often hear them say that their app needs to be scalable so that it can handle the growth of new users. Others say that their app needs to be scalable to handle new features and remain stable. We hear the word so often in different contexts that we thought we would write a blog post to discuss exactly what scalable means to us in the context of Flutter app development and how to achieve it.
Scalable apps should be able to do the two things that we hear most from clients:
1. Handle an influx of new users
2. Support new features without breaking.
But that’s not all! In order to be truly scalable, your Flutter app should also be easy to test and debug and have a composable structure that is easy to understand for all engineers working on it.
In this blog, we discuss what we do to build scalable Flutter apps. After all, focusing on scalable architecture leads to huge benefits that saves engineering teams headaches and ensures that companies can consistently deliver high-quality apps and updates to their users.
Why Build Scalable Code
Scalable architecture is arguably the most important consideration when building an app because it is essentially an insurance policy for your code.
Imagine a world where the engineer who has been around since day one has all of the knowledge about the codebase in their head with no documentation in sight. Not that far-fetched, right? We’ve probably all encountered a situation like this — or done it ourselves.
This may be an OK solution when things are going well, but the second something breaks, you have to hop on a zoom call with the person and could spend hours tracking down the problem. If that person ever leaves the company, the team loses all of that valuable knowledge and the codebase will be hard to maintain.
The reality is: your codebase will not survive if one person has all of the knowledge in their head. That information needs to be documented and built into the architecture so that every member on the team knows how to build new features in a way that will not break the existing infrastructure. Teams need to be able to scale their knowledge.
Scalable code can also help you avoid dreaded spaghetti code. Many developers have a horror story about a legacy codebase where deploying any new code meant breaking some other part of the codebase. Tracking down the problem only led to more issues and it could end up taking longer to find and fix the issue than it did to build out the feature. This does not have to be the case!
Scalable code ensures that you have the right architecture in place to be able to add new features to an application without breaking the rest of it. It also makes opening pull requests and merging them less scary because you can be sure that what you’re contributing will not introduce new problems. Teams need to be able to scale new features.
How to create a scalable architecture
There are four qualities we strive for when creating scalable code.
1. Scalable code is well-tested
If you’re only testing once the app is complete, you’re doing it wrong! Testing should be baked into the development process. There is a huge risk associated with relying only on manual testing right before a scheduled release. Oftentimes critical bugs and stability issues surface, resulting in either a poor quality release or a postponed release date.
You should be able to write tests and reach 100% code coverage of the codebase. Some teams may opt for a lower threshold than 100%, but keep in mind that any part of your code that is left untested could lead to problems. A 90% code coverage threshold may sound high, but for a project with 50,000 lines of code, this means that 5,000 lines of code — a significant amount! — are untested. We created a GitHub Action specifically to measure this and help enforce a designated code coverage threshold.
If it is impossible to reach a high level of code coverage, this is a sign that your code needs to be refactored in such a way as to make testing possible. Untested code is simply unscalable; if some part is left untested, you’re opening up the door to potential bugs or having your app not work the way it was intended.
For a comprehensive resource on testing, our teammate Jorge Coca developed a Caster.IO course on testing in Flutter.
2. Scalable code is easy to debug
Scalable code should be easy to debug because if you have thorough tests, the likelihood of bugs being introduced is very slim.
If bugs do surface, tests you’ve written will likely fail. This can help you isolate the exact part of the code that is causing the issue in a matter of seconds and before the bug impacts end users. If the tests don’t fail and there is still a bug, that’s an indication that once the bug is identified and fixed, a test should immediately be added to ensure that bug never happens again.
Having built-in debuggers and widget inspectors can also make debugging that much easier. Flutter’s built-in tooling provides performance profiling and networking devtools which will help developers spot bugs quicker and then write tests to catch future ones.
3. Scalable code is boring & easy to understand
Codebases should be structured in such a way that any developer of any level — from junior engineers to senior level architects — should be able to understand the logic. In short, you should have a boring codebase. You can achieve this through the following:
- Avoid custom code whenever possible. Scalable code should have very few custom solutions. For example don’t invent your own networking layer if you don’t have to. There are already existing solutions that address this, such as Dio or GraphQL. Getting started on a new codebase should not take hours and should rely on existing tooling where applicable.
- Select and stick with one state management solution. There are already plenty to choose from so there is sure to be one that fits your business needs. Our preferred solution is flutter_bloc because it is predictable and reliable (and it also doesn’t hurt that the creator is a team member here at Very Good Ventures).
- Write clear documentation. When writing documentation, keep in mind that you may be the engineer who might be looking for context weeks, months, or years from now. Write documentation in such a way that if you were to come back to the code in the future, you would be able to jump right back in again, or at least be able to understand the intended function of each portion of code.
- Ensure consistency. Your team should be on the same page when it comes to naming variables, functions, and classes. The codebase also should conform to pre-existing standards whenever possible, including following the Effective Dart style guide and using popular packages when it makes sense. Check out very_good_analysis for our recommended lint rules and analysis options.
4. Scalable code is composable
Scalable applications all have this one thing in common: they should be composed of smaller, independent packages or modules.
This will ensure that each package has a single responsibility and can be tested and reused. This also ensures that engineers can work on the codebase in parallel without interrupting the work of other members.
Each package should also abstract underlying implementation details, which will help development teams iterate efficiently without needing exposure or experience with complex underlying topics. For example, if you’re building a feature and need to access transactions, there should be a library in the codebase that exposes an API to fetch transactions without forcing you to know where and how to load transaction information every single time it is needed.
We typically compose codebases into the following layers:
- Data acquisition: Create data provider packages that fetch raw data from various data sources (such as third party APIs, databases, etc.).
- Business rules: Create domain packages that interact with one or more data providers and apply business rules to them.
- Application itself: This will consist of two parts: 1. UI what the user sees and how the user interacts with the application. 2. Application logic, which are application-specific rules, or what you might think of as state management.
Feel free to experiment with adding additional layers as needed. We have found that having these three layers gives us flexibility to adapt to changing project requirements without having an excessive amount of overhead — the very definition of scalable.
Who should build scalable code? Enterprise, midsize companies, and startups — in short, everyone!
Scalable codebases should be the standard in the mobile application industry, but we wouldn’t be writing this article if this were the case. We hope that this provides some insight into how we ensure that all of the Flutter apps we work on are scalable — and can grow and adapt with our clients’ businesses, not trigger a rewrite six months from now.
If you’re worried that your application’s codebase is not scalable, there’s no time like the present to start implementing habits that will put you on track. For example, if your code coverage is not at 100%, set a goal to raise your current threshold by ten percent within the next couple of weeks. It’s all about implementing good, scalable habits now so that your codebase becomes more and more stable over time.
If you’d like to talk further about Flutter application codebases, contact us! We are passionate about writing scalable code and want to help you do it too.
Felix Angelov and Jorge Coca contributed to this article.