By Vivek Soneja, Architect (Mobile Apps) at PhonePe |
The PhonePe app has expanded from 0 to 150M+ in less than 30 months. Our app is growing uniquely in multiple dimensions. It’s not only adding more business-verticals, but every vertical is adding more features, which in turn are getting more evolved. We believe, if we do not invest enough time to plan the foundational design, infrastructure, and development of the app, we will end up spending most of our time developing the plan for later releases. We have meticulously designed our app from bottom to top to support these ever-growing verticals at PhonePe with reliability and security.
Scaling the App
It’s easier to build smaller Android Apps. Nowadays, we have better tooling to enhance an app’s development. Most developers these days publish their first android app within a few days or a few weeks. On the other hand, developing a professional app, especially one scaling in multiple dimensions is exponentially more difficult. “Scaling the app” refers to designing a continuously evolving app for running on several devices with different configurations, used by various kinds of people, in different languages, at different locations, and on different network conditions. It should be performant, fluid, reliable, and secure.
PhonePe is one of those unique apps which also allows its partners to write an application module for PhonePe. Deploying the code written by someone else on a device owned by someone, yet keeping it secure and performant is simultaneously exciting and challenging. At PhonePe, we have developed an entire platform that would enable the integration of such kind of modules. This platform has emerged as a mini operating system. It solves problems similar to that of an operating system. For instance, since we support integration from various partners, we have built the sandboxing of modules across the PhonePe layers. We have built our user management system as part of the platform. It supports permission management, user-scoped databases, user-scoped preferences and user-scoped network usage, etc. We have also built our resource allocation system, which allocates desired resources as per the need and permissions of the respective modules. We are continuously evolving this platform and making it more enriched, reliable and secure with time.
Platform of PhonePe App
Every app demands its unique path to scale. PhonePe app is required to support various business-verticals getting developed both internally and externally. Because of the nature of the app, we decided to take its design inspiration from the android operating system itself. We have developed the PhonePe app as a Platform, inspired by the layered architecture of a minimalistic operating system. This design facilitates various teams across companies, to develop applications for PhonePe. We call these applications as MicroApps. The entire platform is designed with two core principles – Centralise & Stabilize and Distributed Architecture.
Centralize and stabilize
Everyone in the team should not be solving similar kinds of problems again and again. We should solve them centrally and stabilize them over time. This approach allows us to innovate in one central layer, and everyone benefits from it.
Practically, during the day-today development process, one can only invest limited time to think and make decisions. For instance, how many threads should be allocated to execute the job in hand? More often than not, we make our best estimate, allot a few random number of threads, and never look back. In the bigger picture, that would not only spawn an uncontrollable number of threads in the system but also create the possibility of deadlocks. Having it controlled from the central system would let us dynamically manage the thread allocation for the entire app and possibly prevent deadlocks.
Centralizing the components allows us to invest more time on problems and lets us devise a sustainable solution. We have numerous examples demonstrating the value in this approach. The core principle behind the platform of the PhonePe app is “centralize” as many components as we can and “stabilize” them with time. It has the following benefits:
- Simplifying development: As mentioned above, not everyone in the team has to solve similar kinds of problems every day and invest time in making the same decisions. This kind of design would not only help in faster development but also a more stable and sustainable development. It allows our team to focus on the right set of problems.
- Maintainability: We can work in isolation for most of the core components in the system, and keep on enhancing them without hindering the development of isolated micro-apps.
- Stability: We can make these components more and more robust over time, and hence overall the app can become more stable with time.
While Centralise & Stabilize ensures the growth of a robust platform, Distributed Architecture facilitates isolated and independent development. In ensures our code is distributed across modules and yet keeps it connected. As much as we want our core components to centralize, we also want them to be as independent as possible. MicroApps, framework modules, and most of the central-components should work in isolation. For the same reason, modules in a particular layer should avoid dependencies on modules within the same layer. This principle enables us to work parallelly and independently and helps us with the ultimate goal of continuous and isolated development, continuous integration, and continuous deployment. This approach provides multiple benefits, right from improving build speeds to testing in isolation and rollbacks.
PhonePe Build System: We have written our custom build logic, built configurations to support our platform, and created multiple Gradle plugins for modules used in our app. In PhonePe’s build system, one has to define the layer in which they want to add the module. Moreover, these modules have to be at a specific path and should follow a naming convention. Enforcing the constraints at the build stage itself helps us to organize and maintain our rapidly growing code.
We have written our build system and configurations using Kotlin and Kotlin Scripts (kts). We choose Kotlin over Groovy as most of our team members are more comfortable with Kotlin than Groovy. Additionally, we get extensive support from IDE for Kotlin, which for some reason is not as evolved for Groovy. Having control of the build system, in turn, helps us to develop and innovate on the build level itself. Our custom build system and build pipeline is a large topic within itself.
Anchors: We have various unavoidable use-cases where we need to provide callbacks from the bottom layers to the layers above. For instance, PubSub, the system made to deliver and receive messages from the server, should be able to provide callbacks to the layers on top when their messages arrive. The Push Notification channel should be able to deliver Push Notifications to respective owners in any other layer and the Gson instance might want to collect TypeAdapter defined across the codebase.
In the world of Android, we register broadcast receivers to fulfill similar kinds of requirements. Traditionally, we could have also registered callbacks on the app startup and achieved the same goal, but again, all the modules would have had to embed their code in the application class and to make things worse, we would have ended up creating thousands of objects upfront, which we would probably not have used in this session.
Adhering to our core principles, we encourage the distribution of code in a way that the owners keep the codebase in their respective modules. This principle enables us to distribute code into multiple modules and scale the codebase with existing and upcoming use-cases.
We needed a solution which enables us to achieve the following goals:
- Facilitate us to distribute the code across modules and layers, and gather the required code whenever we want to do so.
- Create objects on-demand, as we do not require all these use cases in every single session of the app.
- Provide a reliable way to interact between layers and across the modules to solve the above mentioned, distributed use-cases.
- It should be simple to use, and reduce our development time.
Keeping the following goals in mind, we have created a framework called Anchors, which helps us elegantly achieve these goals.
We will discuss Anchors in detail in a separate post. To explain its core functionality, it essentially provides us the capability similar to that of Broadcast Receivers in Android. It lets us distribute the code across modules yet keep it connected for required use-cases. We generate this graph at compile time using annotations, and hence it ensures guaranteed callbacks. This framework helps us build on the second core principle of distributed codebase.
Layers of the PhonePe App
The PhonePe app is divided into four layers; the bottom-most layer is called the Platform capabilities layer, this is followed by the PhonePe Core or Kernel layer, then comes the Application Framework layer, and the top-most Application layer . Each layer is developed over the layer beneath it and adds more capabilities for the layer above it. As we go from bottom to top, the layers start becoming more specific to the use- cases.
Why did we choose a layered architecture?
- Separation of concern: It helps us in isolating the concerns of various developments happening at PhonePe and enables our team to work in isolation. The layered architecture, in turn, helps us to gain more confidence in what we are writing and become more predictable with our development.
- Predictability: The layered approach helps us in better prediction of scope and timelines of development. Let’s assume we are going to make changes to the bottom-most layer. It is comparatively easier to predict that this is going to impact a lot of use cases in our app. Hence we know it’s going to take proportionally more time to develop and handle all the edge cases, and also take more time to test as more test cases need to be covered.
- Testability: We need to move towards a more testable code and add more test coverage for our app. At the same time, we need to cope with the speed of development. It’s difficult to define one rule of test coverage for the entire app. Layered architecture helps us in setting different test coverage rules for different layers. We have created the layers in a way that the bottom layers have less UI involvement.
- Inline with build system: The Gradle build system allows only one-way dependence of modules. Our project is already indirectly layered. Visualizing them in four concrete layers helps achieve all the above mentioned goals and at the same time is in line with our current build system.
- Proven Architecture: Lastly, we have taken the inspiration for our designs from an operating system. Layered architecture is a proven architecture with the scale of multiple operating systems. It helps us to build a strong foundation with confidence, and add more use-cases over it.
Each layer of the app is designed with a specific characteristic on the platform. As mentioned above, each layer uses the layers underneath to enhance or add more capabilities for the layers on top. The following are the four layers of PhonePe Platform:
- Platform Layer – PhonePe Independent Layer: The platform layer is the bottom-most layer of the PhonePe platform. This layer is inspired by the Hardware layer of an operating system. This layer will have integrations with platform capabilities such as networks, databases, and encryptions. This layer is PhonePe independent and contains modules that are not specific to PhonePe. This layer has modules such as Databases, Preferences, Network, and Encryptions. We use platform modules (android/iOS), and native modules written in C/C++ in this layer.
- Core Layer – Platform Independent Layer: The core layer has taken inspiration from the kernel layer of an operating system. Hence we also refer to the core layer as the kernel layer internally. This layer is PhonePe specific but mostly platform-independent. This characteristic makes it a potential candidate for the code-sharing layer between various platforms. We have chosen to implement this layer with either platform-specific modules or kotlin multi-platform modules. Kotlin multi-platform provides us with performance comparable to a native code and the same code can be compiled for Android and iOS applications. This layer also integrates with network capabilities in the context of PhonePe. Therefore, it will also have responsibilities such as token management, pub-sub integration, sync management, etc
- Application Framework Layer – Use-case Independent Layer: As the name suggests, this layer has taken inspiration from the application framework layer of an operating system. This layer is built over the core layer and adds platform-specific capabilities for the development of the topmost layer of PhonePe. This layer has modules which are mostly use-case independent and can cater to multiple use-cases. This layer might also have UI involved, and it might understand the specifics and nuances of specific platforms, such as the Android UI system. This layer mostly contains modules written in platform-specific modules.
- Application Layer – Use-case specific layer: This layer has taken inspiration from applications in an operating system. This layer contains all the MicroApps that can execute on the PhonePe platform. This layer supports apps written in platform-specific modules, PWA, and React Native. Micro apps are written both internally and outside of PhonePe. We have created a sandbox environment for all these micro apps to work in isolation, which is a vast topic in itself.
Each MicroApp written for PhonePe can independently choose its stack and design. However, we have published the guidelines for most of the internal apps. Following the standard pattern to develop these MicroApps helps us in two ways. One, we can provide a lot of boilerplate code, making development easy for most of the teams. Second, it becomes easy for team members to contribute to multiple MicroApps.
Ready for the future
We didn’t have this exact design for building a platform from day one. We realized how rapidly PhonePe was growing and over time and acknowledged the requirements for having a strong foundation. Fortunately, our code was modular enough to evolve in the platform. We started a project, internally named “Cassata” to build this platform.
With Cassata, we have laid down a solid foundation for adding the verticals and evolving use-cases in PhonePe apps. The PhonePe platform with a layered architecture provides us the flexibility to work in isolation, continuous integration, and continuous deployment.
Technology for mobile apps is changing more rapidly than ever. Having an extendible and centralized platform allows us to extend the support for most of the upcoming technology. We are developing the majority of our code in Kotlin, we have developed few MicroApps and internal components in React Native, we have integrated and provided the support of Coroutines across the app, and we have developed few modules in Kotlin multi-platform. We have already started using most of the new architectural components released by Android. With our current design, we have not bound ourselves with a set of technology. Our platform is extendible for any upcoming technology in the future.
With a solid base in place, a part of our team has started working towards adding more business verticals and new features for our users. While one part of our team is currently focusing on stabilizing and extending the existing platform. They are working towards making it more reliable, performant, and secure for our users. We believe our efforts will start compounding with time and our users will continue to benefit from the enhancements we are making at PhonePe.
Watch out for our next set of blogs coming soon:
- Anchors – Our way of code distribution while keeping it connected
- The PhonePe Build System
- A deep dive into the making of “PhonePe Switch”