Decoupling digital with micro-frontends and micro-services

TL;DR: This is an opinionated technology solution using NextJs Zones and federated GraphQL. For the techies, there's a fair amount of upfront business context. Bear with me, it's an important part of the story. Especially so, if you're trying to convince your business leaders to invest in a modern architecture like micro-frontends and federated GraphQL.

The large organisation problem

Large organisations, be they modern tech giants or established enterprises, face tough challenges when it comes to ownership of digital assets. As organisations grow and shrink, technology trends shift from monoliths to microservices, shared platforms to value streams, the brunt of the pain is felt by the teams maintaining those assets. We are all too aware of the discomfort of endless restructures in order to align to the latest tech or agile trends: change fatigue.

Where have we come from

We once aligned ourselves by department, or discipline. This led to multi hand-offs, long lead times for change, and helped internal political systems [which add no value] to thrive. These problems were exacerbated by the scale of enterprise organisations.

Product A
Marketing Team
Frontend Team
Backend Team
Infra Team
Security Team
Ops Team
General
r.--On
Product B
o

Teams were not aligned around the goal of improving 'Product A' because their goals were aligned to discipline. The 'Frontend' (or web team) were focused on building websites rather then working with marketers to improve the sales a product.

The modern organisation

The latest trend, is to have teams align around value streams. End-to-end, or front-to-back multidisciplinary full-stack teams. Empowered to own the full lifecycle of an entire vertical slice of an organisation. The reason to align this way is simple: you can effect change without any dependencies on discipline aligned teams (e.g. marketing), because you have that discipline in your team (e.g. your own marketer). No hand-offs, no dependencies, no email tennis - just the efficient flow of work with everyone working towards a common goal.

Product A
Marketing Team
Frontend Team
Backend Team
Infra Team
Security Team
Ops Team
General
re--on
Product B
Potential
Duplicity

The nirvana, or so you might think. There is a problem with this approach: it creates duplicity and over time, divergence. Following vertically aligned principals to reduce dependencies can lead to different design standards, multiple domains and each vertical having their own version of common services. There are now vertical product silos instead of layered discipline silos. Whilst the people aspects can be solved through cultural initiatives like guilds, deploying brand consistent, decoupled applications as part of a coherent digital estate is still a challenge.

Decoupling technology

The Backend

Microservices

Microservices were meant to solve some of these problems. The idea is simple: establish your business domains and have teams aligned to those domains build small services. The problem is, that’s just the back end - and have we really solved the problem? Now we have many services with unclear dependencies and our front ends have to make multiple requests to multiple services. You can streamline this with an API gateway but that doesn’t solve interface chattiness issues or tightly coupled UI components to microservices.

GraphQL

GraphQL provides a schematically defined separation between UI and microservices. There's a misconception with GraphQL that it is monolithic. Whilst it is possible to create a GraphQL monolith, just as you can with a SOA style REST API, there are ways to avoid such undesirable architectures. Apollo's Federation is good for this. I would highly recommend reading the official Apollo docs here: https://www.apollographql.com/docs/federation/.

A great example of GraphQL federation in production is at Netflix. They have written a great blog on the subject here: https://netflixtechblog.com/how-netflix-scales-its-api-with-graphql-federation-part-1-ae3557c187e2

The Frontend

You can solve the brand/design standards issue with a design system (e.g. Googles Material UI) or at a minimum, a style guide. But how do you solve the problem of the tightly-coupled front end?

One app = monolith, multiple apps = inconsistency + confusing IA, and is not optimised for SEO. What you really need is small UIs or components that are independently deployable. Here's some examples of how that can be achieved:

Single App container

This approach has been around for some time and was historically implemented with iFrames. SharePoint 2013 did this with 'App Parts'. Spotify has done it with Electron. It works well for rich interactive single page applications. The Webcomponents standard now provides an iFrame-less mechanism to do this, albeit with varying levels of maturity in some browsers. There are two flavours of this approach.

Framework-less

Cam Jackson from Thoughtworks has written a really detailed article on this approach here: https://martinfowler.com/articles/micro-frontends.html. He advocates the use of vanilla JavaScript to load bundle.js files from various other apps.

With framework

Guy Nesher recently announced https://www.infoq.com/news/2020/12/micro-frontends-single-spa/ the single-spa framework, which is a lightweight framework to glue together multiple SPAs into a single application.

The cons of this approach are complexity in deployment and testing. If you opt for a statically built app you have to have a central deployment mechanism. And if you want isomorphic Jamstack you will need to roll your own.

Separate Apps

This approach is as simple as it sounds: you have completely separate apps. Itworks really well if you have lots of individual apps aligned to journeys, like applying for a bank account. The downside is that inconsistency will creap in without strong design standards, or preferebly, a design system. You also have to solve the IA issue with an approach that is SEO friendly. There are two main ways to do this:

Layer 7 routing

If you want total control and isolated testing you could use a traffic manager to split your traffic based on URL. This means separate apps, that could be on different tech stacks, in different regions and even different cloud providers. This approach puts a dependency on infra engineering, which could be a bottle neck if you have an IA that is immature or emerging.

Zone Apps

There is another way to decouple: Zone apps. This is a NextJs framework based approach and which will work with React only. It allows you to stitch together the routing of multiple NextJs apps running in different locations. The only coupling sits with configuration within the main home app. This is necessary so it knows where to find the other zone apps. Beyond that, the applications are fully decoupled.

There are some downsides with the separate app approach. We have only solved the IA/SEO issue. The UI could easily become inconsistent. To solve that we can use a design system which includes components for standard furniture like header/nav/footer. We could even content control the config by consuming a shared configuration file hosted and versioned on a CDN or a headless CMS.

An example

Our example business (FakeFurniture.com) has the following requirements:

  • The architecture must support a
    • Home team looking after core brand
    • Account team looking after customer data
    • A Product team looking after products, inventory and reviews
  • The architecture must support adding Product teams over time
  • The account team needs access to reviews written by a customer
  • There will be other UIs built by 3rd parties using our APIs
  • Performance and experience must be best of breed
  • All applications must be on brand and share components/content where feasible

These would be fairly common requirements for an organisation with multiple teams and applications. From an architectural perspective, our estate looks a this:

www.com
Home Team
Account Team
Product Team
Accounts
service
www.com/account
API
Gateway
Review
Service
Products
service
www.com/products
Inventory
service

As you can see, the API gateway could easily become the bottleneck for change. The account and product teams could have their own gateways, but that just fragments the estate and reduces the ease of re-use. Likewise, clients have to conform to API specifications and could break as the specifications change.

If we were to take an approach that used a GraphQL schema as the mechanism to decouple frontends from backends via a federation gateway, we could solve a lot of the integration coupling problems. This has the added benefit of simpler integration for 3rd parties UIs.

On the frontend, NextJs Zones are a good candidate to join up our IA. Using Next and Apollo our application now looks like this:

www.com
NEkT
Home Team
Account Team
Product Team
Accounts
service
www.com/account
NEkTß
zone
Review
Service
Apollo
Gateway
Products
service
www.com/products
NEkTß
zone
Inventory
service

For the purpose of this example we are ignoring data stores

In code

You can find a working example in the repo here: https://github.com/richgo/micro-frontend-basic

The repository has been divided to support both logical applications with separate client and server applications that are independently deployable like so:

repo

And yes, that is 8 node apps! (3x NextJs, 5x Apollo GQL).

If you follow the instructions on the readme to get the application running and navigate to http://localhost:4000/ you will find the GraphQL playground. From here you can run queries like:

gp1

What you can see from here is that the reviews are loaded for the current user (which in this case is always Ada Lovelace, because we have no concept of logging in or setting the current user).

If you search for a product by upc "1" you should see the 'Table' product returned, with the same review from Ada Lovelace:

gp2

The code behind shows our mock json data which we are using instead of a real data store.

The same behaviour should be reflected through our (not very polished) UI. You can see this by navigating to http://localhost:3000/account

ui1

And again in the products section by navigating to http://localhost:3000/products/1

ui2

How does it all work

This example is essentially a combination of the nextjs with zones and apollo federation examples available here:

The federated GraphQL server configuration lives here:

const gateway = new ApolloGateway({
  // This entire `serviceList` is optional when running in managed federation
  // mode, using Apollo Graph Manager as the source of truth.  In production,
  // using a single source of truth to compose a schema is recommended and
  // prevents composition failures at runtime using schema validation using
  // real usage-based metrics.
  serviceList: [
    { name: "accounts", url: "http://localhost:4001/graphql" },
    { name: "reviews", url: "http://localhost:4002/graphql" },
    { name: "products", url: "http://localhost:4003/graphql" },
    { name: "inventory", url: "http://localhost:4004/graphql" }
  ],

  // Experimental: Enabling this enables the query plan view in Playground.
  __exposeQueryPlanExperimental: false,
});

You can navigate to any of those servers and they will run just their piece of the graph schema.

Of note, is that changes to the individual servers will not reflect in the gateway on port 4000 until you restart it.

The NextJs Zones config lives in the home-app/client/next.config.js

module.exports = {
  rewrites() {
    return [
      {
        source: '/account',
        destination: `${ACCOUNT_URL}/account`,
      },
      {
        source: '/account/:path*',
        destination: `${ACCOUNT_URL}/account/:path*`,
      },
      {
        source: '/products',
        destination: `${PRODUCT_URL}/products`,
      },
      {
        source: '/products/:path*',
        destination: `${PRODUCT_URL}/products/:path*`,
      },
    ]
  },
}

It is really as simple as that. Next handles all the routing for you from then on.

Conclusion

As you can see, there needs to be considerable DevOps investment to get all the servers up and running. You could handle this orchestration neatly with Google Skaffold (that's for another day!). Sharing components e.g. the header, is something to plan. You either need a design system or some shared library to import components from. A version controlled GraphQL schema registry is also adivsed for a federated graph endpoint.

If you are using React and considering GraphQL for a large application estate that needs a lot of decoupling then this is a solid approach. This is geniunely the first architecture I have seen that can solve consistency and shared services elegantly for a large organisation whilst deliverying blisteringly fast Jamstack experiences. And all with off-the-shelf opensource frameworks.