How to Implement a Scalable Architecture Across Multiple Platforms (Openbank)

Developing the largest banking platform today, serving over 160 million customers across more than ten markets, with development teams spread across different geographies, is a challenge that demands a robust architecture.

Introduction

At ODS, over the years, we have learned that the correct implementation of an architecture allows us to scale both in complexity and in the number of projects in a more agile way. This is true even when it entails a steeper learning curve for those joining the team. In this article, we will delve deeper into this architecture and how it helps us tackle the technical challenges that arise daily.

In our case, we have opted for a modular architecture where certain parts are separated and isolated from the rest at all levels, from the code to the deployment. This comes with a maintenance cost, but the main advantage is that these parts have an (ideally) independent lifecycle. This allows them to grow at different speeds and with different requirements.

Since at ODS we develop banking products, we have functionalities like Accounts, Loans, or Cards, among others. Additionally, we deploy our solution in different markets, each with its own peculiarities. The modular architecture allows us to separate these functionalities both horizontally and vertically through software libraries:

  • Horizontally: Because each functionality is independent.
  • Vertically: Because these functionalities can be extended and overridden.

We must also consider the existence of global and local units. The global units solve the large percentage of the problem, while the local units, if necessary, complete that last mile with the aforementioned particularities.

With this organization, we’ve structured our solution, which now contains over 100 projects and/or repositories per platform.

Key Concepts

Clean Architecture

The concept of Clean Architecture, proposed by software engineer Robert Martin, is based on an approach that allows organizing the code in such a way that it is easier to maintain, scale, and test its various parts. This approach separates business logic from implementation details, including both the user interface and the data layer and dependencies. Here, the data and presentation layers depend on the domain layer (business logic), and there is no relationship between them.

SOLID Principles

The SOLID design principles aim to improve code quality and maintainability:

  1. Single Responsibility Principle: A class or entity should have a single responsibility or reason to change. This helps avoid classes that perform multiple functions and concentrate too many responsibilities.
  2. Open-Close Principle: An entity should be open for extension but closed for modification.
  3. Liskov Substitution Principle: If class B is a subclass of class A, objects of class A should be replaceable with objects of class B without any failures. For example, if I have a “bird” class with the method “fly,” and I create an “ostrich” class that inherits from “bird,” executing “fly” should fail because ostriches do not fly. This example does not comply with this principle, and the class hierarchy should be reformulated to meet it. Instead, by introducing a “bird” class from which “ostrich” could inherit, and a “FlyingBird” class that inherits from “bird” and incorporates the “fly” method, this principle can be fulfilled.
  4. Interface Segregation Principle: Large interfaces that force a class to implement methods they do not use or cannot implement should not exist. If there is such an interface, it should be split into more specific ones.
  5. Dependency Inversion Principle: High-level modules should not depend on low-level modules; both should depend on abstractions. For example, a class should not instantiate a specific “EMailSender” but should work with an abstraction (interface) that allows the implementation to be substituted later without modifying the class that uses it.

Dependency Injection

Dependency Injection is a design pattern that manages object dependencies by injecting them externally, rather than having objects instantiate their dependencies. This makes it easier to replace dependencies and conduct unit testing using mocks.

The ODS Architecture

The software components we build in our architecture can be added and removed like Lego pieces. We achieve this, among other things, by defining protocols, interfaces, and default implementations, allowing any software component to extend and/or override that implementation. Here, dependency injection plays a fundamental role. Practically everything is injectable, not only to create mocks that help us with testing but also to allow the extension and modification of the software.

This philosophy applies across all platforms: API, Android, Web, and even iOS, where, for example, there is no market standard for dependency injection, so we have developed our own. The modular architecture is applied with the same criteria on all platforms; if in iOS we have a specific Accounts repository for Argentina, we do so on the other platforms as well.

Adopting and maintaining a standard is essential to ensure an orderly and easily understandable architecture.

Within each of these components, we implement Clean Architecture. The domain is the central piece; both the data and presentation layers not only depend on it but are also unaware of each other. The code is decoupled, organized, and the responsibilities of each component are correctly assigned. This organization is complemented by a benefit resulting from modularization. At any time, the Data, Domain, or Presentation layer can be replaced by another implementation through dependency injection, allowing for the extension or modification of just one of them without needing to rewrite beyond what is necessary.

Challenges of Our Architecture

The first challenge when implementing this architecture is related to its dimensions: many projects, many repositories, many components. For the success of this architecture, it is crucial to have a good level of automation. This includes everything from templates or starters that standardize the structure of each project to the implementation of continuous integration and deployment across all platforms.

The next challenge is related to communication. We are not only interested in having the architecture be identical (or as similar as possible) across platforms but also in ensuring that the code, at least the public interface, is the same. This is why frontend teams, especially iOS and Android, must work in sync on each development.

Lastly, implementing this type of architecture is easier on some platforms than on others. In the backend/Android world, the adoption of these principles has been established for many years. In others, like iOS, some aspects lack the maturity found on other platforms. We have addressed this with our own developments: a dependency injector, integration with Maven and Nexus to deploy artifacts (xcframeworks), and project management with Tuist, among others.

Conclusion

Although we have achieved greater maturity in our architecture over the past year, it is important to remember that it is not rigid but should have the flexibility necessary to evolve and, more importantly, facilitate the development of quality products.

Regardless of how ambitious our concept of the ideal architecture may be, we must keep in mind that its purpose is to be the foundation on which we build our product.

Related posts

Leave a Reply

Comments (

0

)

Discover more from Tech to be Happy

Subscribe now to keep reading and get access to the full archive.

Continue reading