Micro-application architecture: A guaranteed time saver

In order to guarantee better maintainability and scalability of our applications, we often have to break down our application into a multitude of micro-services with their own logic. These micro-services must be as autonomous as possible and fulfill a specific function in order to be able to reuse them in different contexts.
This architecture is enormously implemented in the Backend side development and therefore more mature. We note for example the emergence of certain registry libraries, API-gateway which make it possible to support such an architecture. However, it is more difficult to architect your FrontEnd application (Angular, React, or VueJS) in micro-services and even less to implement it in the mobile world which by definition is a monolith.
The goal of this article is to show that it is possible to modularize a mobile application, in our case under Flutter, and especially how to build it from several micro-applications.

Global vision

The development of a modular application amounts to drawing a parallel with an assembly of different blocks, each fulfilling very specific functions and having as few outward dependencies as possible. In our case study, this modularity is present in order to have several variants of the same application and therefore to be able in a minimum of time to present the same application with different functionalities.
This micro-application architecture will therefore firstly allow us to present a multitude of variants of an application and secondly allow more robust development. Indeed, such an architecture will allow more optimized maintenance and scalability of the application. The maintenance of the application will only be carried out on the faulty or evolving micro-application and the scalability of the application will amount to adding a micro-application in the application. Consequently, the deployment or the update of functionality will be done only on a part of the application limiting to the maximum an application regression (Defect introduced in software on the occasion of corrections of bugs or any established change).
More concretely, micro-applications allow development using different “repositories” for each micro-application. The latter is in most cases oriented around business functionalities. It also allows separate teams to work on each micro-application.

Before embarking on the implementation of this type of architecture in Flutter, let’s take a step back and analyze the capabilities of cross-platforms in the face of native development. This part will evaluate the performance of the different cross-platform frameworks according to several criteria (Productivity, maintainability…). We will see, subsequently, a comparison based on the same criteria and putting in opposition the micro-application and monolithic architectures.

Cross-platforms VS Native

Cross-platform frameworks allow developers to create mobile applications compatible with multiple operating systems, in this case, iOS and Android. They give the ability to write the code once and then run it anywhere on other platforms, allowing them to release product/software faster, more securely, and with better quality. But let us see if the performance is there.
Native application development avoids the complexity of creating a sustainable product that covers application development across multiple platforms and focuses on building a competent application that stays close to the target platform – Android, iOS, etc.
Cross-platform frameworks, on the other hand, seek to generate an application that reaches as many people as possible by covering many devices during the programming and authoring process.

CriteriaNativeCross-Platforms
CostHigh development costRelatively low development cost
Code reusabilityWorks for one platformA single code can be used on multiple platforms, for easy portability
Access to devices (camera, etc.)The SDK (Software Development Kit) guarantees access to the API of the device without any hindranceNo guaranteed access to all device APIs
Consistency in user interfaceCompliant with device user interface componentsLimited consistency with device user interface components
PerformanceSmooth performance, as the application is developed for the operating system of the devicesHigh performance, but delays and hardware compatibility issues are not uncommon

The question of choosing a native or cross-platform development is largely based on the needs you have and on the project in question.
Finally, the advantages of a native application are:

  • High performance
  • Robust functionality
  • Smooth user experience

And that of a cross-platform application:

  • 70 to 90% reusable code
  • Easy maintenance and updates
  • A wider reach
  • Shorter time to market

However, over the years a multitude of frameworks have emerged, and we can distinguish two types:
Native cross-platform applications: Each operating system has its SDK (Software Development Kit) and its technological stack: Java or Kotlin for Android and Objective-C or Swift for iOS applications. Cross-platform app developers build a unified API running on a native SDK, use native IDEs, and build iOS and Android apps that share the same codebase. Native cross-platform apps are mostly built with Xamarin, React Native, and Flutter.
Hybrid applications: Because the iOS and Android SDKs include advanced web components, it is possible to create parts of an application graphical user interface (GUI) with HTML5, CSS, and JavaScript. Then the developers wrap the code in a “WebView” – a browser built into a mobile app, which makes the content like a good old website. Some hybrid apps even interact with smartphone hardware, although functionality may be limited. The most promising hybrid application development frameworks currently on the market are Apache Cordova and Ionic.
Native cross-platform applications such as Flutter arrived after hybrid applications in order to overcome the lack of performance with the use of Webview and the use of native widgets from a UI / UX point.

Productivity

Productivity during the development of an application can be measured according to several points:
The learning curve: the ability of a new developer on the Framework has developed functionality more complex and faster.
Code reusability: Is the code produced by the developer exportable or can it be used on different platforms?

The learning curve seems to get higher and higher as you get closer to native development. Indeed, it is rather easy to use React Native, especially if you have already used React or Javascript, it is very close to web development with the concept of the component. If we move towards a Framework closer to hardware like Flutter, we notice that learning is quite fast but it is necessary to learn Dart, and reactive programming is not entirely intuitive. Finally, native development such as IOS with Objectif-C or Android with Kotlin remains complex and dense languages ​​to learn. However, there are many more possibilities both technically and functionally.
On the ability to reuse code, cross-platform frameworks clearly have the upper hand with the ability to produce multiple applications with a single code. Indeed, native development forces the developer to write the same functionality on several platforms.

Use of material resources

Native application development offers access to all the hardware functions of the target device (Camera, geolocation, etc.). However, cross-platform applications have access to most of these functionalities either through a bridge or through the browser (for Webview). I am thinking of geolocation, the camera, or even the microphone. Access to hardware resources is not really a weakness of cross-platform applications. Growing communities even facilitate access to them natively or through the addition of plugins. For a classic use of hardware functions, the two solutions are at the same level.

Open source, proprietary, licensed (Cost)

The advantage of the most famous cross-platform frameworks such as Flutter or React Native is that they are open source and free. They do not need to have a particular environment to be used unlike IOS development for example. Indeed, the choice to opt for cross-platform applications is partly due to the cost of IOS development. The latter is rather restrictive and requires us to have a specific environment that increases the cost of development.

Maintainability

Software maintenances can be evaluated under two more precise criteria:

  • Evolutionary maintenance: The ability to change or correct errors more or less easily / quickly.
  • Obsolescence: Does the technology used to have a future? Is it supported by a substantial community?

If we adopt a purely logical vision, the evolutionary maintenance of an application that would be on two platforms (Android / IOS) would take twice as much time on native development than on cross-platform developments.
From an obsolescence point of view, whether in cross-platforms with native Flutter and React or in native Android and Ios, the chances that such frameworks will disappear remain slim. Indeed, Flutter is supported by Google and its community is growing day by day, React Native is maintained by Facebook and has a large community inherited in particular from React. In addition, Android and IOS are the most popular OS around the world.
On the other hand, frameworks such as Ionic based mainly on Webview are becoming more and more obsolete from a performance point of view.
Finally, the choice of the development environment for your application will depend on the needs and requirements of the latter and the capacities of the developers. If the teams come from the WEB world, the transition to React Native will not be confusing. Flutter remains today the closest cross-platform solution to hardware but requires learning a rather new language and poor feedback, Dart. Finally, to produce a quality application that performs well in native development, the learning curve remains high.

The figure above summarizes the advantages and disadvantages of the different technologies that we were able to address based on criteria of performance, access to equipment, and costs.

Monolithic VS micro-application architecture

The choice of architecture depends, again, on the needs and requirements of the project but also on the staff who will work on the application. However, we are going to base ourselves on criteria (similar to the previous part) allowing us to assess the advantages and disadvantages of each.

Productivity

Uses and practices in recent years have evolved towards micro-application architectures. However, a monolithic past remains present in our ways of developing applications and the learning curve to get developers to understand and use such an architecture is not easy. We will see below the various advantages of a micro-application architecture, but it is interesting to note an advantage closely linked to productivity (development): The ability to focus on the functionality to be developed and not in the manner of integration into the future application. From this point of view, such architecture allows a developer to greatly gain in productivity.
When developing in a monolithic architecture, it is in most cases necessary to be aware of the environment in which functionality is integrated and to have knowledge of the business logic of the application. In the case of a micro-application architecture, the scope is restricted and therefore the training for a new developer to integrate the development of an application is shorter with a micro-application architecture.
In addition, this ability to create a stand-alone and independent brick, and not just one more feature in an application, facilitates and optimizes reuse in a multitude of applications. Of course, monolithic architectures make it possible to reuse modules, components, or even services, while remaining within the limits of the application.
Finally, what impacts the most productivity with the micro-application architecture is the installation and the “cabling” of the different bricks. Indeed, it will be necessary to dedicate a team / a developer to the implementation of the central point of the application (the orchestrator). But isn’t the time lost in this implementation saved during the implementation of new functionalities?

Use of material resources

On this point, the adoption of one architecture or another does not impact the available material resources.

Maintainability

Here again, a micro-application architecture presents non-negligible advantages during the maintenance of an application. We can note for example the ability to target an error/bug more easily than in a monolith. Indeed, with the separation of the application being clearer with a microarchitecture, the problem is quickly identifiable. In addition, the implementation of a battery of tests specific to the micro-application simplifies the use of the tests and reduces the risk of regression during an update.
By joining the idea above, the evolution of an application following a micro-application architecture is greatly simplified. Indeed, the implementation of a new functionality amounts to creating a new autonomous and independent brick (micro-application) with well-defined inputs and outputs. The process of evolution in a monolithic architecture requires contextualization in order to be able to implement new functionalities and especially where.

Context and use cases

The added value of a micro-application architecture is having the ability to modularize an application into a multitude of micro-applications. This modularization has several benefits.

Figure 1 – Diagram highlighting the added value of “micro” architectures

Currently, micro-services are more and more popular because they make it possible to distribute the costs on the development level but also on the technical level. For micro-applications intended for mobiles or tablets, the real added value lies in the development plan with the possibility of associating a business logic with a development team (from the micro-service on the backend side to the micro- frontend side application). Technically, mobile or tablet applications will in all cases be a monolith at the end of the cycle.

By implementing a micro-application architecture, the ability to produce an application with different variations becomes simpler. Indeed, this segmentation makes it possible to modularize our application and make it customizable.

This kind of architecture is not suitable for all use cases and especially for all applications. For there to be real added value with this architecture, the application must be large and involve a significant number of developers.

Chosen architecture

The infrastructure of an application, databases, API calls, as well as frameworks, are constantly evolving. But this is not necessarily the case for the business need. Starting from this principle, it makes more sense to orient our way of segmenting our application across the business.
The hexagonal architecture created by Alistair Cockburn ensures the reuse of business logic by making it technique agnostic. Thus, modifying the stack will have no impact on the domain code. A key concept of this architecture is to put all the business logic in one place called the domain.

Figure 2 – Principle of a hexagonal architecture

Hexagonal architecture may be the solution as it tends to isolate an application into three distinct layers:

This layer contains the existing entities in the domain. In this layer, we don’t care how to expose the data to the user or how it will be stored. Inputs/outputs are made using interfaces only. To do this, keep in mind that the business must not depend on anything and therefore never be linked to either the Application layer or the Infrastructure layer.

It is in this layer that the Infrastructure will communicate with the Domain through the interfaces. It is considered here that the Application layer knows the Domain but must never be linked to the Infrastructure. This is where, for business reasons, we can enrich, compare and filter elements returned by the interfaces of the Domain without worrying about the implementation on the Infrastructure side. It is common to speak in this layer of Handler.

The hexagonal architecture considers that the Domain is at the center of the application. In fact, the infrastructure layer will be able to communicate and invoke the objects of the Domain through the interfaces. Be careful, the infrastructure knows the Application layer but not the other way around! In this way, if you want to change third-party APIs, do double writing or change the data model, all you have to do is recreate a dedicated adapter in the Infrastructure layer while respecting the Domain interface.

This architecture is mainly focused on a particular business area. However, in our case, we want to bring together a multitude of business domains (micro-applications) and to be able, from these micro-applications, to create a single application. This ability to make several micro-applications reside in a single one is based on an orchestrator. An entry point to integrate all our micro-applications. Moreover, this is an important element that comes up quite frequently in the segmentation of our applications. Whether for micro-services with the API-gateway system or containers (docker) with docker-compose or Kubernetes, there is always an orchestrator, an entry point to manage all the bricks.

Implementation

The POC carried out was therefore based on the concept of hexagonal architecture. Although the hexagonal architecture allows optimal isolation of the business, it does not allow us to segment our large-scale application. Indeed, this architecture intervenes on a specific business component and not on all the different business logic.
When talking about segmentation, it is always important to think about how to orchestrate all of these segmented parts. In our case, the application is segmented into a multitude of packages, and the orchestrator is represented by the shell (In blue in the figure below). However, we can note several types of packages:

  • Business micro-application: It is a full-fledged graphical interface. It fulfills a specific business function and its entry point is a high-level widget. These packages respect a hexagonal architecture. It is these packages that can be considered as micro-apps because they handle business data (In orange in the figure below). They are part of the business logic.
  • Functional micro-application: It is a widget like a button, an input … reusable in the whole application as well as in all the packages. They are user interfaces transverse to the application and in general to any application. These packages do not include business data but only presentation data. They are part of the business logic.
  • Technical micro-application: It is a micro-application which is present for the technical logic. We find in particular the management of data persistence and the management of types. It can be noted that these packages do not have a UI. They are part of the infrastructure logic.
Figure 3 – Diagram of the micro-application architecture under Flutter

The shell

It represents the entry point of our application and allows us to configure the application according to several inputs. Indeed, the shell retrieves several pieces of information from the environment to build the specific application. There are three files in particular:

  • The configuration file contains all the available variants of the application. This file is the result of a higher-level study in order to determine the different compatibilities between the packages and the variants which are capable of being a builder. Namely, a variant is composed of a description of features and packages specific to the variant.
  • The dependency file is also built upstream with the configuration file in order to have the possibility of choosing the versions of the packages used in the chosen variant.
  • The environment variables intervene just before the build of the application in order to specify a certain number of functional or technical variables. Example: The identifier of the variant or the url of the server …

Routing management

As said before, segmentation implies reunification at one point or another. If we take the example of micro-services, reunification is done in most cases through an API-gateway to provide a single-entry point to our API. In our case, reunification involves the use of routing with dynamic loading of business interfaces (graphical micro-applications) using URLs.

Configuration management

One of the requirements that have been put forward to justify the use of a micro-application architecture is the possibility of modulating an application in several variants. Indeed, this architecture allows us to “build” a different/personalized application for each user.
This variant construction involves the use of environment variables. These allow us to give input parameters at the time of the build of our application. From these variables, we are therefore able to build our application with a certain configuration.
Before the build and use of these environment variables, an additional code generation tool is used to generate the dependency file and a configuration class. These last two generations come from the JSON configuration file statically describing the micro-applications that will be loaded into the application through features.
Currently, this file is created manually but this involves many technical and functional locks. Indeed, we note for example the manipulation of dependencies of different versions or the compatibility from a functional point of view between different micro-applications. This leads us to the conclusion that it is necessary to involve a higher-level tool to manage the generation of the configuration file (JSON file statically describing the functionalities of the different variants).
This file considers several elements:

  • All available features
  • The definition of the micro-applications associated with the functionalities

Following these generations, our shell contains a configuration class representing all the routes available in our final application.

Information sharing (contextualization)

However, our application is, as we have seen, segmented into a multitude of micro-apps that have no idea of ​​the context in which they are used. It was therefore necessary to share this configuration across all of our packages. Thus, any package has the possibility of referring to a configuration package that contains the configuration of our application.
The added value of this package is therefore to give context to the micro-apps. Example: A micro-app could view, edit, delete data. However, the configuration informs us that the loaded variant does not allow deletion. The configuration package allows us to access this information anywhere in our application and especially in any micro-app. Even if the latter is far in the navigation tree. This saves us from passing the configuration to each parent/child (See diagram below: Figure 3).

Figure 4 – Difference in dependency injection methods

The management of types/models within the application is done in two parts. We notice the business models and the technical models. To better manage the DTOs, the models associated with a business logic are placed in the concerned micro_app. However, the more technical models and purely associated with the management of the application are provided in a separate package managing the configuration of our application.
So, each package has the possibility to add context and link between our packages.

Scope of use of the different packages

When creating a micro-application associated with a business domain or a business subdomain (particular operation on a business domain for example), we can see the latter as an empty shell that will be fed internally by business logic (handling of data specific to the business, for example). In addition, independent external packages will be able to provide additional applications or technical functionalities to our business logic.
Thus, as can be seen in FIG. 2, we find the business logic at the center with an application layer making it possible to manage, among other things, the data. At the periphery, the use of external packages makes it possible to relocate transverse user interfaces and purely technical code.

Implementation of continuous integration

The micro-application architecture has been designed as it is the capacity to be modular, to have the possibility of managing several variants of the same application. This modularization process has been greatly simplified by continuous integration. The project presents entry points such as the chosen variant or the URL of our backend. But we can very well imagine having a multitude of variables as input. These are used to define a configuration class that lists all the input variables. This class is consumed by the application to access the execution environment of the application and thus provide the appropriate application.
Thus, with two commands, we have the possibility to choose for example the variant of our application and to optimize the loading of the micro-applications. What is interesting to note here is this ability to be able to vary the application upstream of the build. This was made possible thanks to the use of the “environment_config” package and the code generation project. Environment (decision) variables during continuous integration come from a continuous integration pipeline and therefore are easily modifiable.
Finally, the implementation of continuous integration was performed using the Forge and more particularly of Gitlab CI / CD. All you need is a YAML “. GitLab-ci.yml ”to describe how we will modularize our application and create our output artifact. Once the new version of the application pushes on a certain branch (in our case “master”), a script will be launched.
As we have seen previously, in the case of the choice of variants, the environment variables have decision-making and non-descriptive character. In fact, the description of the variant (its configuration) is done manually in the code or else is generated by a higher-level tool that will have the capacity to consider functional requirements, for example. It is therefore recommended, if we had to have somewhat complex configurations, to use the environment variables only for decision-making purposes and to declare in the code or recover the complex objects.

Process of creating a micro-application architecture

The main axis on which we must look when we want to respect a micro-application architecture is the business aspect. Indeed, our architecture is based on hexagonal architecture (oriented towards the business domain). So, the first step is to identify the different business components involved in the application.
Business packages have the specificity of handling and presenting data specific to business logic. In addition, the latter must be as independent as possible from external business logic. Occasionally, a business package may need to access information from another business package. The goal is not to reimplement the way to go looking for a business component. It is necessary to go through the business package to retrieve information. This guarantees data integrity while keeping a centralized business code.
It is possible in the same business logic to decouple the latter into a multitude of business packages. Indeed, once the business component has been identified, we can notice that it can be separated into several functionalities. We can identify, for example, a visualization package and a business logic editing package. They can be split because they do not fulfill the same functions.

Identification of transversal interfaces (functional and technical)

During the migration process, it is necessary to quickly identify the technical base of the application and the elements that are reused in several or all business packages. Indeed, we can notice in the studied application that we have a “core” that represents the technical basis of the application and more precisely the configuration of the application. This logic was placed at the level of the app_management package (package already present in the reference architecture and essential for the management of configurations and routing) because it concerns the application logic and must be accessible by all the packages. You can also identify or create elements (widgets) that are repeated in the application. We can cite for example “inputs” with a certain style, buttons, etc …

Example of use cases

During the development of an application, several functionalities can be implemented such as geolocation, QRCode scanning, or the establishment of a local database. To make it easier to identify and design packages, here are some examples of features associated with a package type.

Setting up a database

When it is necessary for an application for there to be data persistent, a local database must be set up. We would tend to instantiate a single common database with a package that would manage that database. This package would then be a technical package fulfilling a very specific role in accessing and making data available.
However, if we try to draw a parallel with the micro-services, we can notice that good practice is to have a database by micro-service and therefore logically by business logic. Thus, setting up a database is ideally done in the associated business package. This once again allows for a single interface that accesses and modifies the data.

Geolocation

Geolocation is a feature that can be used by a multitude of packages because it does not involve a business in its logic. It does not need the context in which it is going to render information to the function. Thus we can implement this functionality using a functional package (transverse functionality).

Configuration management and storage

It is common to have to store configuration data relating to the operation of the application or to user preferences. All this management must imperatively be found in the package managing the configurations and routing of our application. The “app_management” package is an essential package for the operation of our future application and it is possible to add your own configurations.

Conclusion

The implementation of a micro-application architecture allowed us to highlight the possibility of modularizing or customizing an application through different variants. And this in the simplest way possible with the use of continuous integration. The fact of segmenting our application is to see it as an assembly of bricks and not as a single block, allows us to open doors on the ability to set up development by a business team that would have to support the entire chain (from the backend with their micro-service to the front end with their micro-application).
Thus, the maintenances and the evolutions of our products will be done in an optimized way and will be focused on the functionality to maintain or to evolve. We will no longer be concerned with integrating a feature into an application but with developing an independent and reusable feature in any application.
All this to build a common and mutualized development stack within Berger-Levrault.

Ready to start with our micro-app framework, go to the forge : https://gitlab.forge.berger-levrault.com/bl-drit/flutter-micro-app-architecture

More ...

Retour en haut