The core of Shiprocket’s Frontend runs on our largest and oldest project that uses the decade-old framework, AngularJS. As much as we’re proud of how far we’ve come with it, it was a high time for us to move on.
As we diversified our products and started shipping out features rapidly, it became integral to migrate our core project to newer and modern versions of Angular. At the same time, we knew the increasing size and complexity of this project could bring in enormous technical debt and scalability concerns.
So alongside migration, we also wanted to abstract some modules and pages from our core project into standalone projects that can interact with each other. Turns out we found a reasonable solution by breaking Shirtpocket Frontend into separate projects and running them all on the same domain.
But this didn’t come along easily. There were loads of hurdles we had to overcome and tons of dedicated work our engineers had to put in.
So in this roundup, I’ll walk you through how we decided the right path for migrating Shiprocket Frontend. I’ll also discuss how we implemented a pseudo micro frontend architecture that allowed us to run independent Angular projects on the same domain.
But First, a Primer on Shiprocket Frontend
Shiprocket Frontend forms the base (and technically the face) of everything that happens in India’s largest B2B logistics startup. For context, this is what powers our admin panels, the seller panels, seller’s onboarding and registration, uploading shipments etc. On the client or user-facing side, It’s like that centerpiece of the puzzle that holds the bigger picture intact.
The only problem was the technology behind it. It was powered by AngularJS, version 1 of Google’s Frontend framework that came out more than 10 years ago.
Need for Migration
We don’t have a culture of bashing old and stable technologies or engineering techniques simply because they’re not the buzz in the town anymore. What works right for us, allows us to scale in a cost effective manner and gives us a good time to market is openly welcomed in our tech stack.
But the problem with AngularJS is, it’s days are numbered. It’s a tad bit humorous how the Angular team is trying their best to support it, but we know it’s dying badly amidst the community.
This renders the entire code of our project legacy. With newer versions of Angular coming out in the blink of an eye, we were worried that one day AngularJS would just go MIA and wreak havoc on us.
Secondly, we know that newer versions of Angular advocate a component based architecture against AngularJS’ traditional MVC architecture. We have used Angular on other projects at Shiprocket and our UI engineers have loved the modular approach it brings in creating high performance Single Page Applications.
So all in all, doing away with AngularJS for Shiprocket Frontend was essential.
But Complete Migration was a No-Win
When you’re migrating a project this big, the steps involved are just as big. So if we were to do a complete migration on Shiprocket Frontend, here’s what we had to deal with.
Large Migrations are Decision Driven
We were migrating from a 10 year old framework to its year old counterpart. This meant, as a first step, we needed a strong foolproof process to plan the migration.
This meant preparing an in-depth documentation for the migration process followed by an intensive discussion amidst the engineers and team leads. Then generating a feedback loop to keep QA and product teams on tabs. And finally allocating engineering and QA resources accordingly.
It sounded like a lot of planning was needed alongside time investment before migration could even begin. For a startup that’s scaling at a pace like ours, time is an imperative resource and we wanted to be sure that there isn’t a better and more optimal solution we could use instead.
Engineering Resources are Expensive
We had experts who could spin up a new Angular project, copy modules and services from Shiprocket Frontend to the new project, then tweak those modules to match newer Angular’s syntax, refactor legacy code and eventually get Shiprocket Frontend up in the LTS of Angular.
But there’s more. We would also need the devops team to spin up new staging and production servers with relevant environment configuration. And finally, have the QA team test the new project like a monkey.
Do you see where I’m going with this? This implementation could be a months-long process that would take an insane amount of engineering resources.
Not only that meant a halt in what we had planned for our coming sprints, but migrating the entire project in a single go didn’t sound like a smart decision anyway. At least not right now.
Lastly, remember I mentioned we wanted a more modular approach to the growing size and complexity of Shiprocket Frontend? All things combined, here’s what we decided to go with.
Partial Migration with Pseudo Micro Frontend Architecture
We observed two key components in our Shiprocket Frontend project. First, the Admin part and second, the Seller part. Logically it made sense to abstract these parts away from the project into separate Angular projects.
Now this would mean we would be migrating and modularizing away a ton of code from our original Shiprocket Frontend project. Since they’re fresh projects now, we would obviously use newer versions of Angular.
So we made a Shiprocket Admin project on Angular 10 for all the admin pages. And another project called Shiprocket Seller on Angular 12 for the seller pages. This approach gave us the best of both worlds. Since we moved important segments away from legacy code, it gave us more security and scalability for our frontend.
Since it was comparatively lighter than migrating the entire project, only limited engineering resources were spent in the process. Therefore we didn’t lose our usual pace in rolling out features in other products.
But different Angular projects meant we now needed to host modules of the same
application on different domains or subdomains, right? Yes, but we didn’t want that. These are barely different modules of the same system that interact with each other.
So essentially, our problem boiled down to having multiple Angular projects that are
intertwined together. And all of these can interact with one another. Most importantly, they can do so by running on the same domain. This gives our projects somewhat a micro-frontend architecture, although not completely.
If all of this sounds overwhelming, let’s explore how we went about implementing this.
Implementation
We know that Angular eventually serves your single page application via a single index.html file. Now we have more than one Angular project on the same domain. The problem is how does the server know which project to call? All three projects are separate single page applications built via Angular residing in different git repositories. So which Angular project should run and when?
We differentiate these projects on the basis of their routes. Earlier, we had everything, the admin, seller panel and seller login running on Shiprocket Frontend. When an admin or seller wants to login to Shiprocket Frontend, we redirect the route call to the Shiprocket Admin Angular project. We do this by stuffing a distinguishable keyword in the route. So if this keyword is present in the route, our devops team knows that a different Angular project is supposed to be requested than the one the user is currently on.
For this we have added custom commands to generate the production ready dist folder. Here’s what that command looks like:
"build-custom": "ng build --prod && mv \"./dist/sr_admin/index2.html\" \"./dist\"",
And we make the corresponding change in our angular.json file:
"outputPath": "dist/sr_admin",
"deployUrl": "sr_admin/",
These keywords are in a way mapped to what Angular project they represent and this setting is configured on our servers. So let’s take the scenario that an admin user logins on Shiprocket Admin. The user is then taken back or redirected to the Shiprocket Frontend project. This is because remember, we have only partially migrated now. Some pages on the admin still happen to sit on our original Shiprocket Frontend.
Inside Shiprocket Frontend, we have the menu links for the admin to navigate to various pages. So naturally the app has a sidebar and a header. Now some of these pages belong to Shiprocket Frontend, as in they’re part of it’s own client side routing.
However, some also belong to Shiprocket Admin. For instance, pages that are part of the Shiprocket Admin project include seller products, pickup address, dg goods, training panel, seller report, upload awbs, etc. Remaining admin pages are part of the Shiprocket Frontend project.
So if we request a page of Shiprocket Frontend, our AngularJS project’s client side routing handles everything for us. However, if we request an admin page that’s sitting on Shiprocket Admin, we pass the distinguishable keyword on the route. So now a fresh request is sent to our servers. And due to this keyword, the server knows it has to send back a relevant page of Shiprocket Admin instead. So it pulls the latest code from Shiprocket Admin’s Github repo, generates an Angular build, hops inside the dist folder and sends back it’s index.html file.
However, it’s worth mentioning that the index.html file we wish to serve could conflict with other Angular projects. We’re already using index.html in Shiprocket Frontend, so in order to differentiate it from the index.html file generated inside Shiprocket Admin, we use a different name for this html file. Notice that the build command I demonstrated earlier generates the index.html file and also renames it as index2.html. So now our server knows that index.html is the entry point for Shiprocket Frontend whereas index2.html is the entry point for Shiprocket Admin.
This process goes on back and forth when we’re traversing through pages of the admin. Let’s take the second case of a new seller who’s getting onboarded on Shiprocket. Since seller onboarding pages after registration sit on the Shiprocket Seller project, the seller is redirected to Shiprocket Seller project for onboarding.
Under the hood, a similar process is followed where a keyword is appended to make the route distinguishable. Consequently, the server is configured to trigger the relevant angular project depending on the route requested on the client side.
Finally there is a direct or simple case that we couldn’t neglect. Amidst all that complexity, what happens when a user directly requests a page of any of the projects? For instance, if a user directly requests /login or /register, what does our server conclude? Which project does it need to open?
Well, in this case, the server fires up the Shiprocket Admin project directly since this route is part of the Siprocket Admin project itself. This is handled through load balancer on the server which ensures that we’re only pushing the relevant code on the server. Therefore, all our JS and CSS files for each project are differentiated from each other. So if the user directly hits the /login url or any other internal route of the application, we let the application’s internal routing handle that navigation. So in essence our server simply fires that Angular project instead of navigating back and forth between multiple projects. That simply resolves down to the case of running a single Angular project on a domain. All internal routing of the projects had to be kept intact without any overengineering introduced.
So Are We Talking about Micro Frontends?
You might think that using the above approach, we’re moving to a micro frontend architecture. Well in a way we are, but not completely.
Each angular project can be thought of as a micro frontend or a micro-app in itself. And these micro frontends put together form the entire frontend of Shiprocket. So every piece fits perfectly in the puzzle to form the bigger picture.
So to some extent, you can say we have moved to micro frontends. We meet the following criteria when it comes to a perfectly decoupled micro frontend architecture:
- Individual projects are now easier to build, scale, test, deploy and manage.
- Projects are owned by independent teams
- Allows partial migration, ie, migrate an old app whilst running a new one simultaneously
However, there are some points that aren’t crossed out on the checklist. For instance, since all these apps run on the same domain, we aren’t isolating localstorage and browser cookies. While we don’t let them conflict either, but there isn’t a mechanism to isolate domain specific
browser behavior yet.
Secondly, we aren’t using embeds or iframes where a microapp sits inside another
microapp and both can communicate easily via postmessage API. Our implementation is solely route based.
So it’s best to say we moved to a merely pseudo micro frontend architecture!
The Way Forward
Based on our current implementation, we have already started working on a pure microapp that links these projects together. Currently we’ve tied these projects together via routes. The visible testament of these routes are the Sidebar and Header. Each project has its own Sidebar and Header that shares these routes together.
We’re working on a completely decoupled micro frontend Sidebar and Header which will remain common in all of these projects and render their interconnected links. Since this will be a small project, it will give us the flexibility to fix architectural problems more easily. It will also be the perfect case to work out how iframes fit into the micro frontend architecture.
Finally, we’re swiftly moving towards migrating all our admin pages to Shiprocket Admin so we can safely migrate Shiprocket Frontend to newer versions of Angular.