Using Monorepo To Cut Costs for White-labelled App Development
An AOT team discovered how a monorepo pattern could help scale development of hundreds of customized retail ordering apps
It’s all about themes and variations.
I’ve always been curious about how large teams produce different applications with similar themes, so I’d been particularly happy to join an AOT project for a client with just those requirements, knowing that I’d get to see how we’d handle all those many workspaces.
Over several years we’ve been developing a retail delivery application that lets businesses offer their own mobile and web applications to online customers. Behind the scenes, there’s also a back-office application to handle the orders, inventory, user management, and billing.
Our client wanted us to create a separate mobile application for each of its own clients. At the same time, we wanted our development process to be efficient, which meant we definitely didn’t want to manually create hundreds of mobile applications to cover every retailer.
When Covid-19 magnified customer demand, we needed to quickly scale up the app. That’s when switching to a monorepo pattern proved crucial to support the rapid scaling we needed to create a new version of the app.
Here’s how we did it.
Introducing Version 2
Once Covid hit, online ordering spiked, so we focused on creating a better ordering platform with a seamless user experience. After our own brainstorming and discussions with the apps’ existing clients, we decided the second version of the apps should have an entirely fresh look and be:
- Intuitive and user friendly
- Fast and reliable
- Offering more white-labelling features than the first version
- Able to use microsites so the app could be fully customized
- Able to function with minimal coding and maintenance effort
The Initial Plan
The initial plan for this second phase was to create multiple workspaces for different projects as we were not sure about the client’s technology selection. We particularly wanted to avoid running different kinds of front-end technologies and databases for the same project.
We decided we’d use Angular for our core development, Ionic for the front end, and Node.js and MongoDB for the back end. We also followed conventional practice by developing our ordering and admin apps in separate workspaces.
Taking a Monorepo Approach
Keeping scalability in mind, we also decided to try out monorepo architecture, replacing traditional polyrepo architecture.
There are many reasons we decided to go with the monorepo approach:
- Visibility: We can look at the company’s entire codebase in one place, making it easier to gain a clear vision of the complete system.
- Consistency: We’re able to share ESLint config, UI web components, utility libraries, documentation, and so on, with all projects.
- Dependency Management: If a developer breaks a shared file, monorepo tools such as a dependency graph can identify affected projects and files.
- Shared node_modules: A single node_modules folder can be shared across multiple applications if they’re placed inside a single monorepo codebase.
- Simplified CI/CD: Monorepo allows easier automation of the build system.
- Efficiency: A single command can build or run multiple workspaces at the same time.
Although this approach helps to process multiple projects, there are some disadvantages too. As you add projects, the codebase grows, and so do its artifacts, which can slow down the processing of data and resources, which in turn increases build and compiling time.
So in order to run their own monorepos properly, tech giants such as Meta, Google, and Microsoft have developed their own set of tools. Our AOT team, however, didn’t have the bandwidth to build or buy such tools, we relied on a build system in order to define workspaces and config files, create styling files, and package.json files to list each workspace’s essential packages.
Tools to the Rescue
Turborepo is an open-source build system with computational caching and parallel task execution. It was written in Golang and doesn’t have any plugins.
Nx is a feature-packed build system with computational caching and parallel task execution just like TurboRepo. It was written in TypeScript and has an inbuilt CLI called NX Console that can automatically generate boilerplate code.
The decision to go with Nx
Comparing the features of different build tools, Nx is the clear winner with numerous extra features and, studies show, a speed nearly nine times faster than Turborepo. Nx follows a similar design ideology to Visual Studio Code, making Nx the VS Code of build tools. Nx has a build system that is easier to understand for developers who are used to single repo architecture.
- The Nx project graph
Nx uses a project graph to reflect the source code available in the repository. The nodes in the workspace are defined in the project.json file, which gets automatically mapped when a new project is added to the workspace. We can also prompt Nx to analyze an existing project in order to produce the needed project graph.
- The Nx metadata
Nx has a wide variety of metadata that can be configured and updated according to the user’s needs. It has integrations to IDEs such as VS Code and WebStorm.
- Code sharing across multiple applications
Nx lets you share reusable components from different frameworks such as Angular and React. Nx has a common libs folder where we can write common code and components. We can even write an application that contains both React and Angular components.
- Nx Console
Nx Console is the UI for Nx. It’s a VS Code extension that provides a GUI for operations to handle multiple workspaces. Selection of a command from the list of actions on the left-hand side of Nx Console will run that command in the command line terminal. It helps developers to perform operations without learning the commands by heart.
Here’s a section enlarged:
And here’s the execution message:
The Nx workspace is divided into different sections:
- /apps/ contains the application projects. This is the main entry point for all applications. All lightweight items are kept inside the apps folder. Individual HTML and CSS files are kept here.
- /libs/ contains the library projects. All the common libraries, components, and services are kept inside these libs. This libs folder is imported to multiple projects inside the apps folder.
- /tools/ contains scripts that act on your code base. This could be database scripts, custom executors, or workspace generators.
- /workspace.json lists every project in your workspace. This file acts like a configuration file for handling all the projects in a single file providing visibility of the overall picture.
- /nx.json configures the Nx CLI itself. It tells Nx what needs to be cached, how to run tasks etc.
- /tsconfig.base.json is a base config file that mainly contains aliases for different applications created in Nx and aliases for the libs folders that help to simplify the JS/TS imports. It also helps in configuring the compilerOptions. Some of the compilerOptions include sourceMap enabling, JS target settings, and setting the baseurl.
Below is one way to set up your workspace directories. You can see individual apps (whitelabelled, microsite-1, microsite-2) placed in the /apps/ folder and common files in the /libs/ folder:
Version 2 of the app required a client application to be developed using Angular and an additional node server on top of the existing node server to handle APIs relevant just to the client app. There were many new workflows to be handled as part of version 2 for which we had only a limited time to work on. With the help of the Nx multi-project workspace, we created the client app and node server as two projects inside the same workspace. Nx supports the generation of boilerplate code in some languages and frameworks. This helped our initial code set up by reducing the development effort needed to create a folder structure for the project. It also has a central config file called workspace.json to manage all workspaces in one location. By the very nature of white-labelled applications, customizations and branding for each client were handled using configurations provided by a single back-office application. The main types of customization included logos, primary and secondary colours to enhance the theming, full listing management, inventory management, and staff management.
Even though we offer a wide variety of customizations, some clients wanted an entirely unique application that strictly followed their own brand guidelines. We understood that within a single project, it would be difficult to achieve that granular level of customizations, but by using Nx we could easily set up a new project in which we reused all the necessary sections, mainly components.
So here are the applications we would need to finish our implementation of version 2:
- One client-side white-labelled customer-facing web application written using Angular.
- One client-side hybrid mobile application written using NativeScript, an open-source framework to develop mobile apps with functionalities written in Angular.
- One node server application that uses MongoDB as the primary database.
These were the initial app requirements, but beyond that we had plans to add a number of microsites that relied on this shareable codebase.
It took us four to six months to develop our first white-labelled applications under version 2, and within a month or two after that, we were able to set up a completely new microsite for a client to provide an entirely different UI experience. Our monorepo approach greatly reduced development time, build time, and compiling time, and effectively addressed a developer’s potential pain points when dealing with larger codebases.
The core functionalities were written in the /libs/ folder so that other applications can share the common items from /libs/ if needed.
The move to a monorepo development approach has helped us to create a scalable codebase to which we can add any number of projects and make the development process effortless. This has saved a lot of development time and effort in maintaining the codebase. Application-wide refactoring of the code, managing multiple libraries, and synchronizing various pull requests would have been a challenge once the codebase grew. With the monorepo approach, these challenges were addressed.
About the Author
Arun S is a senior full-stack developer at AOT Technologies specializing in developing highly scalable mobile and web applications. While working on this latest project for over two years he has focused on managing and implementing new workflows as well as creating intuitive designs and mockups. Outside of work Arun enjoys listening to music.