Our micro-front-end web architecture

Preface

Currently, I am the Front-End Engineer on our development team. My job, outside of developing much of the front-end portion of our new product development, is to focus on overall Front-End architecture and the shared reusable components of the code which can be easily used by BE/Full-Stack developers who may not be really interested in the front-end. In the current state of things these back-end Devs are able to pull off quite a bit on the front-end side of things without being super familiar with the underlying frameworks and technologies involved by using the cookie-cutter templates (see below) and libraries I've helped to create. Very rarely do they even have to create new LESS styles spin up entirely new apps and generate the UIs that their products demand.

While I am the only official Front-End Engineer at Aver, I certainly did not design and build this all on my own. I have the pleasure of working with many brilliant full-stack developers who both coded and provided insights that helped shape this architecture. The architecture detailed below is one which was started ~3 years ago and has grown and morphed significantly over that period.

The current state of things:

  • We have over 18 distinct AngularJS Single Page Web Applications which make up a single product/site.
  • These applications are small, loosely coupled, designed for a single purpose and are easy to scale.
  • Each App lives under a top-level URL path such as localhost/App1/, localhost/App2/, and localhost/App2/dashboard.
  • One app, home-web, owns the absolute top-level localhost/
  • Each app has it's own deployment cycle
  • Typically each one of these front-end apps is accompanied by 1 or more back-end APIs who serve data to the app but any FE app is capable of utilizing any back-end api. The user-management-api for instance is used by many different fe applications.
  • Navigating between apps executes a full-page reload but navigating between pages in a single app does not.
  • Navigation between the apps happens often and seamlessly.
  • The apps all share a single CSS Library so the look and feel across all of the apps is cohesive and navigating between them feels as if it were one monolithic application. This library has it's own Repo and NPM private registration. When the style library gets updated, all of our apps need rebuilt in order to pick up the updated styles or they'll be out of sync with one another and the transitions between them could potentially not feel succinct.
  • They share 90% the same exact code.
    • The in-house shared code is pulled out into distinct AngularJS Modules/Components that have their own GitHub repo and NPM private registration.
    • The 3rd party libraries are included via NPM and build into a vendor.bundle.js file using WebPack.
    • The unique package.json 3rd party dependencies of any individual app are between 0-3, and these are only added when an app has a unique dependency that won't be used across the entire suite. If we find that every app is pulling in a particular library, we'll generally just make that a dependency of our ui-scaffolding library and pull it out of the individual app package.json files.
  • I've written a tool using CookieCutter which allows new front-end apps to be generated with via command line so that developers starting on a new app can start writing the code that gives the app purpose almost immediately without dealing with all of the typical boilerplate setup cruft. Here's what it does automagically:
    • Prompts the user for any app-specific info such as:
      • App name
      • App base-url /user-management
      • Github repository info - It will automatically checkout the repo, generate the app into the folder, and make the initial commit of the app so you're ready to create a branch and start working immediately.
      • Includes demo assets - Example views, filters, directives, services and the accompanying unit tests to model after.
    • Sets up all of the scaffolding which has to happen before you really start writing the code you want to write:
      • Angular app/module, Routing, Session logic, UI-Scaffolding.
      • Webpack Configuration
      • README.md
      • Package.json
      • ESLint Configuration
      • Unit test framework
      • Docker config
      • travis config
    • Provides examples of Views, Components, Filters, and Services.
    • Follows our code-standards and best practices.
    • Integrates the app with our internal routing & dev-sandbox tools.
  • In Production/Test/E2E/deployed environments, for each app, we run a pre-built static version of the Application sitting in a docker-container running nginx.
  • In Development, for each app, we run the Webpack dev server in a container. On top of that we run an in-house dev tool that sits in front of all of this to handle routing on localhost to the different applications as well as container management tools.

The good

Download only what you need:

Some of our Applications are what I would call "heavy" and have quite a bit of custom LESS, HTML, and JavaScript. Not all of our Users use, or even have access to, all of the other parts of the overall product. With our setup, the Users only download exactly the apps they are going to be using. If they don't hit a particular part of the application, they won't download the source for it. This does mean that the first time a user hits an individual micro-fe-app that they will experience a slightly longer load time while that app's JS, CSS, HTML and images are downloaded. Subsequent sub-pages hit on that app will used the cached sources.

Deploy only what you need:

Any one of our Apps can be developed on, tested, and deployed completely independent of the other Apps. This gives us a lot of flexibility!

Breaking up ownership:

There are clear divisions of ownership and responsibility of code and products between developers. As our team grows, it is likely that our app's products will be owned by separate Development Teams. With our current setup that would be extremely easy to organize as there is a clear division of top-level products as well as components and libraries. For instance we could have a single team assigned to the Bootstrap/CSS library who can make application-wide changes to the look and feel of the app without causing much trouble to other teams assigned to the different top-level applications.

Sunsetting an application is clean and easy

Sometimes it's decided to shut down or remove a part of the Application. With this setup, doing so is incredibly easy! Let's say we wanted to get rid of a part of the app localhost/ExperimentalApp. All we'd need to do is shut down that individual service and it's no longer accessible. We'd then delete the Repo, remove any broken links in any other apps (I should figure out a better way for this), and it's gone! We're not left with old CSS, Angular Components, views, routing logic or anything else that might linger after the pages/apps are gone. I've worked on some pretty large, long-running, web-applications at prior jobs and I can tell you that the amount of lingering CSS is kind of terrifying. Generally the styles are so potentially coupled with other pages or areas of the site that people are afraid to delete it. So what you end up with is thousands of style definitions or JS functions/components that are no longer being used; but they live on forever, getting bundled up and downloaded every time a user hits your site.

Developers only check out the code they're working on

If you're a new developer assigned to work on one particular app, you don't have to clone down every single part of the web application just to work on the little piece that you plan to touch. You can pull down, and start up, everything in self-contained pieces.

The Bad

Keeping apps in sync can be a challenge

Let's say we update our UI library and want to propagate those changes out to all of our FE Applications. Every FE App will need individual rebuilt & redeployed. But it gets worse. If we pin the UI Library to a specific version in our package.json file for each app, then we have to manually go through every FE application and update the version, open an PR, get someone to review it, merge it into master, rebuild & redeploy. This isn't so bad when you 3 Micro-services, but like I said above, we have more than 15. This gets exhausting! Especially when you get though all of this once, and then realize you need to immediately update it for something else. If anyone knows a solution for this please let me know!

We could just not pin the version (ui-library: "*") or only pin to the next major version (ui-library: "^3.2.1") but then that makes other things a bit more complicated, like testing. For instance if someone pushes a new shared library update, any app that gets built-deployed from that point forward, who doesn't have the version pinned, is going to pull-in the updated library. This isn't always clear that it's happening. If things break, it could be totally unrelated to the app code changes which initiated the deployment and have the developer scratching their head and sending them on a while goose chase. This is no bueno!

I'm actively looking for a solution to this problem so if anyone has any ideas let me know!

Docker isn't great for webpack containers with file watching

One thing that we run into is the overhead of running many docker containers for the different front-end applications. Locally, each one is not running a static compiled version of the Application. It's running a full-on webpack dev server with file-watching and live reload. In fact here is the line we run webpack-dev-server --host 0.0.0.0 --port 80 --progress --colors --inline --hot. I think there are some definite optimizations that can be made to make sure that we're not running the live-reload file-watching over the node-modules folder or other parts of the code that don't require it. I've made some of these optimizations but I think there's room for improvement yet.

Typically, if you run the dev tool that's basically "Fire up all of the things" locally, your macbook fan will kick on full blast and sound like a harrier jet for 10-20 minutes. Even after everything is started and is sitting idle, it's not uncommon that your fan remains on max-speed and computer runs hot. One thing we've done to combat this problem is being able to mark either a FE or API container as "Static". When done on front-end containers, the tool uses the static pre-built container for that app and skips running the webpack dev server or file watching. The end result is a lightweight instance of that Application, with the caveat that you're unable to make changes in the code and have them be reflected in the page.

If anyone has tips on running many webpack dev servers in docker containers locally in a more efficient way I'd love to hear about it!

The shared FE code is still download once-per app

Right now, the generated/bundled vendor.bundle.js?hash file which is deployed for each FE app is 100% identical to the vendor.bundle.js file for any other FE app. This giant chunk of JS, which includes AngularJS, Angular UI Router, and many other fairly large libraries is downloaded over and over again as you navigate between our Apps. I know this is a totally solvable problem through proper Webpack configuration & deployment setup but I've yet to hunker down and work out a solution. If anyone has any tips on how to solve this problem please share.

We're tightly couple to AngularJS

I think in a more ideal world our shared libraries and components are not dependent on AngularJS and rather just plain ol vanilla JS. Right now every piece of shared JS is an AngularJS 1.6.1 component/filter/service/etc. This breaks the paradigm that our micro-services are technology/library agnostic. If we went a more generic route for these libraries we could have a team build a new micro-fe-app in whatever FE framework they wanted, such as Angular2, React, or Vue while still utilizing that shared code. Some solutions to this problem include:

  1. We could work towards re-writing the scaffolding code and shared libraries in a framework-agnostic format so anyone could use it, like plain ol vanilla JS. This seems simple enough for services but I'm not quite sure how to handle templates where you have library specific portions such as ng-repeat or ng-if.
  2. We could use a tool like SingleSpa which allows you to run multiple different front-end frameworks in a single page. I haven't spent time looking into this further but I'll need to very soon.
  3. I could leave all the shared code as Angular components and use tools like Angular2React and ngImport to include those components right into my ReactApps. The major downside of this approach is that it will require new specialized tooling for every receiving library. For instance if someone chose to create a Vue front-end, Angular2React wont be much help.
  4. I could use Predix to re-write eall of the components as predix components which are supposed to be framework-agnostic.

If you're familiar with other solutions this problem, or ways of migrating yourself out of it, I'd love to hear!

So what's next?

Well, I've begun prototyping and experimenting with moving from AngularJS@1.6 to ReactJS. By that, I don't mean migrating all of our front-end-micro-services over, but rather coming up with a strategy where our legacy AngularJS apps live side-by-side with the newer ReactJS apps. I'll be covering that process in a WIP follow-up post that you can find here: http://coreysnyder.me/it-has-begun-moving-from-angular-1-6-to-reactjs/

Micro-Front-End Architecture Resources:

Other topics to add:

  • Authentication
  • Public versus Private pages

Corey Snyder

Corey Snyder

Senior Front-End Engineer for Aver Inc.. I have independently developed & released multiple video-games. I play Ice Hockey, I race FPV Drones, and I love my Subaru WRX STI.

Read More