Sep Nasiri leads the UI Infrastructure Team at Upwork. The Infrastructure Team creates and maintains many of the tools and libraries used by the various frontend product teams.
Over the last few years our focus has been on modernizing the Upwork stack to improve performance and reliability. My attention was recently drawn to a note about “Micro Frontends” in the thoughtworks Technology Radar, an approach we have been following as part of our modernization effort. I’d like to share the challenges we have faced and how we have gone about solving them. It might be worth noting that we have not traditionally used the term ‘micro frontends,’ but I have a feeling this may change as a result of the more formal documentation referenced above.
Upwork began to modernize its frontend sometime after we started to modernize our backend. For the backend, we chose to embrace a microservices architecture. The reasons for our choice of microservices (and our modernization effort on the whole) is described in this blog post written by our CTO.
We felt that many of the same reasons for using microservices on the backend could be applied to the frontend: enabling the various frontend teams to iterate at their own pace, then releasing when ready; risk isolation; and, the ability to experiment with new technologies easier.
Many of the requirements for micro frontends are the same for microservices: Monitoring, healthchecks, logging, instrumentation, metrics, etc. However, breaking up the frontend monolith also presented its own challenges. In frontend design, the user must be presented with a UI that looks and feels consistent. There are UI elements that appear on many pages. The separation of concerns isn’t always as clear as it is for backend services.
The first issue we encountered when we started experimenting with micro frontends was how to handle our navigation menu. Our navigation menu is not simple static HTML; it can vary from user to user. For example, the items a user sees in the navigation menu is based on a complex permissions logic and changes depending on that user’s context. Much of this logic was embedded within the frontend monolith. Untangling it to create a clean library would be a major refactoring project—something we did not wish to invest in. In addition to this, we wanted to start using a more modern PHP framework for our micro frontends, which further complicated things.
The first method we used to solve this problem was really just a hack. We created an endpoint in our monolith that would serve only the navigation HTML. The new micro frontend would request this endpoint, forwarding the needed authentication information, then just drop the response into its output. We could have done something similar with an AJAX request but we chose not to. (The memory containing the reasoning for that has since been freed by my brain’s garbage collection). This quick hack may have been a suitable solution to start but it was certainly not going to work in the long term. After all, we wanted to replace the monolith, not make it a dependency of our new frontend.
The idea to make the new micro frontends delegate the navigation logic made sense to us. Instead of delegating to the monolith, we decided we needed a Navigation Service. We built an Agora service that would contain all the messy permission logic and return the structure of our navigation. We wanted to remove business logic from the frontend anyway. The service does not return HTML but only a logical (JSON) representation of what items should be displayed. This data is processed by a frontend library that produces HTML. To prevent duplicating logic, we modified our monolith to also use the new service.
Having the monolith act as a service to unblock micro frontends, while we built proper services to replace it, proved to be a useful pattern that we have reused.
UI Components Library
The next challenge we tackled with micro frontends was presenting a consistent look and feel, while also isolating risk. Several factors compounded the difficulty of this. First, we wanted to take the opportunity to modernize our client-side libraries. We planned to utilize Bootstrap for our CSS and HTML, along with AngularJS, moving away from our jQuery-based custom framework. Secondly, we were in the process of changing the look of the site to coincide with the launch of the Upwork brand.
Slight variations in the same component from one version to the next are considered an acceptable tradeoff to maintain risk isolation. Other times the variations are too great to be considered acceptable. These situations demonstrate one of the clear downsides of the micro frontends approach. To address this, each frontend is updated to support both the old and new look, and a feature toggle allows us to switch from old to new. How we toggle features is a topic for another post.
Even prior to our frontend modernization effort, we had some URLs that were served by a different system. For example, our Visitor Site (what you see if you are not logged in as an Upwork user) was served by an application and server cluster separate from the main application. This was handled by our Nginx load balancer. However, the proliferation of micro frontends required constant changes to the Nginx LB to be made manually. The high risk of load balancer updates combined with the fact that these changes were made to the config manually made updates slow and ruled out more advanced deployment patterns (Blue/Green, Canary).
With the fundamental theorem of software engineering as our guide, we introduced a new Nginx load balancing layer (which actually replaced individual AWS ELBs each frontend was using previously). The new Nginx setup differed from our main (previously mentioned) load balancer in that its config is automatically generated. Each instance of a micro frontend registers itself in Consul. Additionally, it adds the paths that it serves in Consul’s key-value store. A tool called Consul Template generates a new Nginx config when there are updates.
Migrating to a micro frontend architecture presented some challenges but the benefits of modernizing Upwork’s frontend along the way made it worthwhile. Modernization will help future-proof our site and streamline certain services to deliver a better, more consistent user experience for Upwork’s 17 million global registered users.