Composing Frontend Applications with Micro Frontends at Tray

Andrew Jones
Building Tray.io
Published in
4 min readMar 23, 2022

Andrew Jones is a Senior Frontend Engineer and Technical Lead at Tray.io. He happens to have worked here for the last six years, so he’s seen firsthand our platform’s transformation from a monolithic application to Micro Frontends.

In the beginning…

The Tray frontend was initially built as a monolithic application and was served by two frontend engineers (including myself), but as new engineers onboard, new teams formed, new products built, it became obvious that our monolithic approach to the Tray platform was not going to keep up with the rapid scale of Tray as a company, and it needed a major rethink in how we build and maintain applications with the level of quality we were aspiring to.

One of our major problems during this growth period was increasing the confidence in our changes. Due to the tight coupling between our UI code, it was common to make a seemingly innocent change in one area of the codebase, only to introduce a regression in an unrelated part of the platform.

Users of the Workflow Builder will be familiar with the Step Editor — which allows the editing of a single workflow step, but what may not be obvious is that the Step Editor has a second home (and owner) in another product — Connector Builder.

The first iteration of Step Editor was maintenance hell.

It was originally designed more like an extension of the Workflow Builder. Largely because the Connector Builder didn’t exist yet. The tight coupling to the Workflow Builder in turn forced some architecture decisions on the Connector Builder due to the Step Editor’s reliance on technologies like Redux, and a number of smells were introduced such as conditionals targeting specific applications.

The Step Editor needed a rethink, but it also needed to lay the groundwork for other libraries to follow. Our goal being that teams could compose applications from a pool of shared standalone features which could be used anywhere interchangeably .

We leaned heavily on a lot of existing Micro Frontends content already published, and from there we drafted:

Four requirements for new libraries

Single responsibility

A library should focus on one task, and relate to an entity of Tray. We start this process by writing a single sentence of what the library should accomplish.

Portable

A library does not know or care about its environment and should work regardless of where it is used. The library should have a set list of inputs and outputs, and that should be the only way to get anything in or out of the library — this means no reading from global state!

This isn’t limited to code either, visually it should adapt too. We can use responsive design here to make the library look great inside modals, sidebars or even full screen.

It does as much heavy lifting as possible

The library should be easy to use by any team. The ultimate goal of any library should be a one line import with minimal amount of required data. Can the library fetch extra API data itself without requiring it from the consumer? Can the library accept a commonly used input and provide the same format as output without needing extra parsing? This is the kind of friction that’s great to avoid if possible.

Off by default

With the introduction of Organizations and Workspaces in the platform, user roles and permissions are a key part of the front-end. Libraries should be designed around the minimum access allowed, with features turned on when the user role is high enough. This applies everywhere too — it’s much easier to turn on rather than off.

This opens more possibilities for teams to reuse common functionality, while also using their own tools and patterns specific to their application.

Take the following example of changing the authentication in the workflow builder:

  1. Step Editor receives data about the current workflow step from the API
  2. It passes in an Admin user role to indicate this step can be edited.
  3. The Step Editor can do some heavy lifting and fetch the authentications available from the API.
  4. It uses the fetched authentications and step data to show a dropdown.
  5. When an authentication is selected, Step Editor fires a callback signalling to parent components that a change has occurred.
  6. Workflow Builder responds by saving a new version of the workflow.

And the following example of using the Step Editor as a preview window in the Connector Builder:

  1. Step Editor receives a workflow step hardcoded in state
  2. No user roles are provided and the Step Editor is by default in readonly mode
  3. It does not need to fetch authentications (as they can’t be changed anyway)
  4. The step editor can just show a label with the current authentication (if any)
  5. The step editor isn’t given a callback, and so the application does not know or care about changes

We now have two consumers of the Step Editor, reusing the same core functionality, providing two different experiences just by changing the input.

Taking it further

What I’ve demonstrated here is a single library, able to be consumed by any number of applications. Libraries are designed to be composed into any other library too. Giving us a new set of building blocks to compose applications with — a design system of features if you will.

Next we will be discussing how we applied the same practices across the new Workflow Builder, combining multiple libraries to build one application.

--

--