Micro-Frontends in Angular - a practical approach
We at Halodoc, have fully embraced Microservices architecture in order to overcome the limitations of monolithic backends. However, the frontends which complement these Microservices are usually SPAs (Single Page Application) and are feature-rich and over time they become big frontend-monoliths. Micro Frontends is an architecture style used to overcome the limitations of monolithic frontends.
An architectural style where independently deliverable frontend applications are composed into a greater whole
Why Micro-Frontends (MFE)?
We have a built a web application called
Control Center, it is basically a centralised console used by our internal teams to support operations in our services - Pharmacy Delivery, Tele-Consultation, Insurance, Labs and others.
The frontend part of
Control Center is an Angular SPA (Single Page Application)
and over a period of 4 years it has grown into a big frontend monolith.
Below are some of the problems we faced:
- Slower build times - build time increased steadily (taking close to 20 - 30 mins)
- Slower development time - multiple teams were working on a single codebase and a single product simultaneously. But because of the coupling and complexity of the monolith, we were stepping on each other's toes.
- Slower release cycles
Control Center growing out day-by-day, it was high time we adopt Micro Frontends to scale.
- Higher productivity for the people developing and maintaining the system, through more efficient communication and coordination, and higher quality.
Conway’s Law applies to modular software systems and states that:
Any organisation that designs a system (defined broadly) will inevitably produce a design whose structure is a copy of the organisation’s communication structure.
The above statement can be paraphrased as - a software system whose structure closely matches its organisation’s communication structure works better (defined broadly) than a system whose structure differs from its organisation’s communication structure.
Microservices / Micro Frontends embraces Conway's Law to leverage the power of distributed teams.
2. Smaller, more cohesive and maintainable codebases
3. The ability to upgrade, update, or even rewrite parts of the frontend in a more incremental fashion than was previously possible
Critical Assumptions made
MFE approach for
Control Center was chosen based on the following assumptions:
Control Centeris largely used by operators on the Desktop
- The number of users will not exceed much as we are catering only for internal Halodoc operators
- They have very good connectivity
- No SEO / SSR requirements.
Different approaches evaluated
- Each MFE as a lazy-loaded remote module
- Each MFE as a lazy-loaded web component
- App Shell loading MFE with build-time composition
- Each MFE is its own Angular app (reverse composition)
Below are the details of the evaluation:
We decided to go with option 4 (reverse composition) as it suited our use case - we didn't require SEO or any demanding performance budget as this is used internally by our teams and not by public customers. Also, considering the limitations of the Angular Framework support to Micro Frontends.
Following are some of the criteria considered while splitting Control Center into multiple MFEs:
Separate Business Entity ( For example Pharmacy Delivery can be a separate business entity )
Rate of change & size parameters also played their part in decision-making
High-Level MFE Architecture
Fig (a) shows the existing architecture, where our frontend is a monolith angular app, which talks to our API Gateway or BFF (Backend for Frontend) and the BFF talks to our micro services.
Fig (b) shows the MFE architecture, where the frontend monolith angular app is split into multiple angular applications.
MFE Design Details
Each MFE will be a separate Angular application (SPA).
All the layout components namely header, footer & menu will be extracted as libraries and each MFE will make use of these libraries to construct its page. (We host our shared libraries in our own npm private registry using verdaccio )
Since each MFE is a separate SPA, and a full-page load is required in order to navigate from one MFE to another.
Handling cross-cutting concerns
All services, components and guards with cross-cutting concerns will be extracted into a library or multiple libraries. For example authentication, authorisation, user, menu, notification, and other such services. Each MFE will rely on these in order to fulfil cross-cutting concerns.
Inter-communication among MFEs
Control Center client app is not maintaining any state that stays beyond the browser session. ie we’re not maintaining any state in local storage. For the most part, the
Control Center app relies on the server state for its state management.
However, communication between MFEs can still happen via url path and query params. For example: instead of passing the order object, we pass order id in URL path or query params, and the receiving MFE has to make a call to the server and get the order details.
Note: In cases where we cannot communicate via URL, we can make use of browser storage mechanisms like session storage or local storage. With this we will get into the perils of shared memory communication and used as a lost resort.
Based on the URL path requests will be forwarded to respective MFEs. for ex
/labswill be forwarded to Labs MFE which is an S3 bucket. Basically, the URL would be
<main-domain>/labsbut would have rendered index.html from labs S3 bucket.
These configurations will be done in the CDN. (And, the / will be configured to point the starting MFE i.e Dashboard MFE)
Requests which start with /api will be forwarded to Batavia-proxy service through load balancer as shown in the above diagram.
Security headers will be forwarded using Amazon’s Lamba function.
When it came to migration to the new architecture, we didn't go with the approach of re-writing the monolith from the scratch, as it posed the following problems:
- You won't be able to use it until the new system is developed and functioning as expected.
- This approach of re-building the whole thing from scratch with new architecture, requires a lot time and effort.
- You won't be able to test your new architecture on prod until whole thing is migrated.
- During this period, you cannot have any non-trivial features delivered on the current system, so the business will have to wait till its built on the new system.
In order to overcome the above risks / problems, we choose to go with Strangler Fig Pattern - a popular pattern for incrementally transforming a monolithic application into micro services.
The Strangler Fig Pattern offers an incremental, reliable process for refactoring code. It describes a method whereby a new system slowly grows over top of an old system until the old system is “strangled” and can simply be removed.
So in our case, first we targeted a relatively low risk feature or module and converted that into a Micro Frontend and deployed to production. We monitored, tested and collected the feedback and iterated on it. And, once we were confident, we proceeded to the next module and so on. This way we migrated our entire monolith into Micro Frontends. (Currently, there are 12 MFEs)
Our experience post the migration has been positive so far, our build times have reduced significantly - from 20 - 30 mins to around 5 - 8 mins. Also, with independent repos and deployment pipelines, we're seeing that there are lesser cases of teams stepping on each other toes and teams have more control over their deployments and has helped the teams to deliver faster.
As mentioned earlier, this approach to Micro Frontends, works well for the use case or requirements mentioned above in the blog, and for customer facing websites which demand much better user experience without any kind of page loads after the initial page load, you will have to look for other options or come up with your own solution, as so far, there has been no official support by Angular for implementing Micro Frontends.
However, that might change with Angular supporting Webpack 5 - Module Federation. As using this we may be able to create an Angular-based micro frontend shell using the router to lazy load a separately compiled, and deployed micro frontend. We will keep you posted on the developments in this regard.
We are always looking out for top engineering talent across all roles for our tech team. If challenging problems that drive big impact enthral you, do reach out to us at firstname.lastname@example.org
Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our Tele-consultation service. We partner with 3500+ pharmacies in 100+ cities to bring medicine to your doorstep. We've also partnered with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allow patients to book a doctor appointment inside our application. We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates Foundation, Singtel, UOB Ventures, Allianz, GoJek and many more. We recently closed our Series B round and In total have raised USD$100million for our mission. Our team works tirelessly to make sure that we create the best healthcare solution personalised for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.