Micro Frontends for Java Microservices

Originally published at auth0.com.
Micro Frontends for Java Microservices

Microservices have been quite popular in the Java ecosystem ever since Spring Boot and Spring Cloud made them easy to build and deploy. Things have gotten even easier in recent years with the proliferation of new Java frameworks built specifically for microservices: MicroProfile, Micronaut, Quarkus, and Helidon. Not only do these frameworks provide an excellent developer experience, but they also tend to have built-in Docker support. They even work with GraalVM, so you can compile your apps to native code!

To be fair, MicroProfile isn’t a framework; it’s a spec with many implementations. In fact, both Quarkus and Helidon have MicroProfile flavors you can use.

Today, I’ll show you how to build a Java microservices architecture that leverages micro frontends for the UI. The backend will use Spring Boot and Spring Cloud, while the frontend will use React. You can also use Angular or Vue if you’d like.

Prerequisites:

You can install JHipster with npm:

npm i -g generator-jhipster@7.9.3

Java microservices with Spring Boot and JHipster

JHipster is an application generator that creates a Spring Boot backend. You can configure it to use SQL or NoSQL databases, plain ol' Spring MVC, or reactive with WebFlux. It also generates a UI for your REST API and offers you the choice of Angular, React, or Vue. Last year, I showed you how to build reactive Java microservices with Spring Boot and JHipster. In this tutorial, you’ll build a gateway with Spring Cloud Gateway. Then, you create a blog microservice and a store microservice, each with its own database.

JHipster microservices architecture

This microservices architecture had one major flaw: the UI for it is a monolith, with all its files on the gateway. This isn’t good from a loose-coupling point of view because changes in a microservice might require changes in the UI. Instead of being able to deploy the microservice independently, you have to deploy the gateway too.

Today, I’m proud to show you how you can solve this problem with micro frontends. JHipster recently added support for micro-frontends. Microfrontends provide a way for you to remotely load and execute code at runtime so your microservice’s UI can live in the same artifact without being coupled to the gateway!

JHipster microfrontends architecture

In the previous paragraph, you might notice I spelled micro frontends three different ways. The current literature is all over the place on this one!

I’m going to use "micro frontends" for the remainder of this post since that’s what Cam Jackson used in his Micro Frontends article on Martin Fowler’s blog.

A quick introduction to Module Federation

Webpack’s Module Federation is one of the best-known implementations for micro frontends. Its ModuleFederationPlugin allows a build to provide or consume modules with other independent builds at runtime. It even allows you to share libraries between frontends to reduce the size of remote bundles.

Zack Jackson is the creator of Module Federation and recently collaborated with Manfred Steyer to create Native Federation. This means you can use micro frontend concepts with any build tool, not just webpack.

Why should Java developers care?

I think micro frontends are a fascinating architectural concept. Microservices weave everything together on the backend with protocols like HTTP and gRPC. With micro frontends, it’s all HTTP. You can see your app get stitched together by watching your browser’s network console and seeing remote modules load.

I’ve encountered quite a few monolith UIs in my time as a consultant. The backend was a beautiful microservice architecture, but it was all tightly coupled on the frontend. There’s a good chance many Java developers don’t care about the UI because they just work on the beautiful backends. However, if you consider yourself a Java web developer, micro frontends are as revolutionary as HTML5!

And that’s the beauty of this tutorial; you don’t have to write any micro frontends. JHipster can create them for you!

Micro frontends in action with JHipster

I used a JDL (JHipster Domain Language) file named reactive-ms.jdl to create the reactive Java microservices tutorial I mentioned earlier. You can see this file online in the JDL samples repository.

I copied this file to reactive-mf.jdl and changed a few things for this tutorial:

  1. Enabled micro frontends by adding microfrontends [blog, store] to the gateway’s definition.

  2. Changed the client framework from Vue to React.

  3. Added clientFramework react and Cypress to each microservice.

  4. Changed the service discovery to Consul.1

  5. Added a deployment section for Kubernetes.

1 We’ll default to Consul in JHipster v8.

JHipster’s JDL allows you to specify different client frameworks, but micro frontends will only work if you set the same one for all your apps.

Run the following command to create a new directory for your micro frontends project:

take micro-frontends-jhipster
# mkdir micro-frontends-jhipster && cd micro-frontends-jhipster if you don't have take installed

Download both the -ms and -mf JDLs and compare them in IntelliJ IDEA. You’ll need IDEA’s Command-line Launcher for the idea command to work.

jhipster download reactive-ms.jdl
jhipster download reactive-mf.jdl
idea diff reactive-ms.jdl reactive-mf.jdl
Reactive microservices refactored to micro frontends
Additional K8s deployment
The JHipster JDL Plugin is a handy tool for working with JDL files.

Micro frontend options: Angular, React, and Vue

JHipster has support for the big three JavaScript frameworks: Angular, React, and Vue. All are implemented using TypeScript, and a newly generated app should have around 70% code coverage, both on the backend and frontend.

There is also a Svelte blueprint, but it does not support micro frontends at the time of this writing.

Build Java microservices with Spring Boot and WebFlux

To generate a microservices architecture with micro frontend support, run the following command:

jhipster jdl reactive-mf.jdl --monorepository --workspaces

The last two arguments are optional, but I expect you to use them for this tutorial. Without the monorepository flag, the gateway and microservices would have their own Git repos. The workspaces flag enables npm workspaces, which are kinda like having an aggregator pom.xml that allows you to execute commands across projects. It also makes it so there’s only one node_modules in the root directory. To learn more, I recommend egghead’s Introduction to Monorepos with NPM Workspaces.

If you want to use Angular, append --client-framework angularX to the command above to override the JDL value:

--client-framework angularX
angularX is a legacy JDL value from back when JHipster supported AngularJS and Angular 2. We will change it to angular in v8.

If you’d rather try out Vue, use the following:

--client-framework vue

Run your reactive Spring Boot microservices

When the process is complete, cd into the gateway directory and start Keycloak and Consul using Docker Compose.

cd gateway
docker compose -f src/main/docker/keycloak.yml up -d
docker compose -f src/main/docker/consul.yml up -d

Then, run ./gradlew (or npm run app:start if you prefer npm commands). When the startup process completes, open your favorite browser to http://localhost:8080, and log in with the credentials displayed on the page.

You’ll be redirected back to the gateway, but the Entities menu won’t have any links because the micro frontends it tries to load are unavailable.

The gateway’s entities are unavailable

Start the blog by opening a terminal and navigating to its directory. Then, start its database with Docker and the app with Gradle.

npm run docker:db:up
./gradlew

Open a new terminal and do the same for the store microservice.

You can verify everything is started using Consul at http://localhost:8500.

Consul services

Refresh the gateway app; you should see menu items to navigate to the microservices now.

Gateway entities available

Zero turnaround development that sparks joy

At this point, I’ve only shown you how to run the Spring Boot backends with their packaged React apps. What if you want to work on the UI and have zero turnaround that sparks joy? ✨🤗

In the gateway app, run npm start. This command will run the UI on a web server, open a browser window to http://localhost:9000, and use Browsersync to keep your browser in sync with your code.

Modify the code in gateway/src/main/webapp/app/modules/home/home.tsx to make a quick change. For example, add the following HTML below the <h2>.

<h3 className="text-primary">
  Hi, I'm a quick edit!
</h3>

You’ll see this change immediately appear within your browser.

Gateway quick edit

Remove it, and it’ll disappear right away too.

Now, open another terminal and navigate into the store directory. Run npm start, and you’ll have a similar zero-turnaround experience when modifying files in the store app. The app will start a webserver on http://localhost:9002, and there will only be one menu item for product. Modify files in the store/src/main/webapp/app/entities/store/product directory, and you’ll see the changes in your browser immediately. For example, change the wrapper <div> in product.tsx to have a background color:

<div className="bg-info">

The UI will change before you can Cmd+Tab back to your browser.

Store edit

The backend has quick turnaround abilities too, thanks to Spring Boot devtools. If you modify a backend class, recompiling it will cause Spring Boot to reload your component lickety-split. It’s pretty slick!

A look under the hood of micro frontends

When you’re learning concepts like micro frontends, it’s often helpful to look at the code that makes things work.

The gateway’s webpack.microfrontend.js handles setting up the @blog and @store remotes and specifying the shared dependencies and components between apps.

gateway/webpack/webpack.microfrontend.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

const packageJson = require('../package.json');
const appVersion = packageJson.version;

module.exports = ({ serve }) => {
  return {
    optimization: {
      moduleIds: 'named',
      chunkIds: 'named',
      runtimeChunk: false,
    },
    plugins: [
      new ModuleFederationPlugin({
        shareScope: 'default',
        remotes: {
          '@blog': `blog@/services/blog/remoteEntry.js`,
          '@store': `store@/services/store/remoteEntry.js`,
        },
        shared: {
          ...Object.fromEntries(
            Object.entries(packageJson.dependencies).map(([module, version]) => [
              module,
              { requiredVersion: version, singleton: true, shareScope: 'default' },
            ])
          ),
          'app/config/constants': {
            singleton: true,
            import: 'app/config/constants',
            requiredVersion: appVersion,
          },
          'app/config/store': {
            singleton: true,
            import: 'app/config/store',
            requiredVersion: appVersion,
          },
          'app/shared/error/error-boundary-routes': {
            singleton: true,
            import: 'app/shared/error/error-boundary-routes',
            requiredVersion: appVersion,
          },
          'app/shared/layout/menus/menu-components': {
            singleton: true,
            import: 'app/shared/layout/menus/menu-components',
            requiredVersion: appVersion,
          },
          'app/shared/layout/menus/menu-item': {
            singleton: true,
            import: 'app/shared/layout/menus/menu-item',
            requiredVersion: appVersion,
          },
          'app/shared/reducers': {
            singleton: true,
            import: 'app/shared/reducers',
            requiredVersion: appVersion,
          },
          'app/shared/reducers/locale': {
            singleton: true,
            import: 'app/shared/reducers/locale',
            requiredVersion: appVersion,
          },
          'app/shared/reducers/reducer.utils': {
            singleton: true,
            import: 'app/shared/reducers/reducer.utils',
            requiredVersion: appVersion,
          },
          'app/shared/util/date-utils': {
            singleton: true,
            import: 'app/shared/util/date-utils',
            requiredVersion: appVersion,
          },
          'app/shared/util/entity-utils': {
            singleton: true,
            import: 'app/shared/util/entity-utils',
            requiredVersion: appVersion,
          },
        },
      }),
    ],
    output: {
      publicPath: 'auto',
    },
  };
};

The blog’s webpack.microfrontend.js looks similar, except that it exposes its remoteEntry.js, menu items, and routes.

blog/webpack/webpack.microfrontend.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
const { DefinePlugin } = require('webpack');

const packageJson = require('../package.json');
const appVersion = packageJson.version;

module.exports = ({ serve }) => {
  return {
    optimization: {
      moduleIds: 'named',
      chunkIds: 'named',
      runtimeChunk: false,
    },
    plugins: [
      new ModuleFederationPlugin({
        name: 'blog',
        filename: 'remoteEntry.js',
        shareScope: 'default',
        exposes: {
          './entities-menu': './src/main/webapp/app/entities/menu',
          './entities-routes': './src/main/webapp/app/entities/routes',
        },
        shared: {
          ...Object.fromEntries(
            Object.entries(packageJson.dependencies).map(([module, version]) => [
              module,
              { requiredVersion: version, singleton: true, shareScope: 'default' },
            ])
          ),
          'app/config/constants': {
            singleton: true,
            import: 'app/config/constants',
            requiredVersion: appVersion,
          },
          'app/config/store': {
            singleton: true,
            import: 'app/config/store',
            requiredVersion: appVersion,
          },
          'app/shared/error/error-boundary-routes': {
            singleton: true,
            import: 'app/shared/error/error-boundary-routes',
            requiredVersion: appVersion,
          },
          'app/shared/layout/menus/menu-components': {
            singleton: true,
            import: 'app/shared/layout/menus/menu-components',
            requiredVersion: appVersion,
          },
          'app/shared/layout/menus/menu-item': {
            singleton: true,
            import: 'app/shared/layout/menus/menu-item',
            requiredVersion: appVersion,
          },
          'app/shared/reducers': {
            singleton: true,
            import: 'app/shared/reducers',
            requiredVersion: appVersion,
          },
          'app/shared/reducers/locale': {
            singleton: true,
            import: 'app/shared/reducers/locale',
            requiredVersion: appVersion,
          },
          'app/shared/reducers/reducer.utils': {
            singleton: true,
            import: 'app/shared/reducers/reducer.utils',
            requiredVersion: appVersion,
          },
          'app/shared/util/date-utils': {
            singleton: true,
            import: 'app/shared/util/date-utils',
            requiredVersion: appVersion,
          },
          'app/shared/util/entity-utils': {
            singleton: true,
            import: 'app/shared/util/entity-utils',
            requiredVersion: appVersion,
          },
        },
      }),
      new DefinePlugin({
        BLOG_I18N_RESOURCES_PREFIX: JSON.stringify(''),
      }),
    ],
    output: {
      publicPath: 'auto',
    },
  };
};

Build and run with Docker

To build Docker images for each application, run the following command from the root directory.

npm run java:docker

The command is slightly different if you’re using a Mac with Apple Silicon.

npm run java:docker:arm64
You can see all npm scripts with npm run.

Then, navigate to the docker-compose directory, stop the existing containers, and start all the containers.

cd docker-compose
docker stop $(docker ps -a -q);
docker compose up

This command will start and run all the apps, their databases, Consul, and Keycloak. To make Keycloak work, you must add the following line to your hosts file (/etc/hosts on Mac/Linux, c:\Windows\System32\Drivers\etc\hosts on Windows).

127.0.0.1  keycloak

This is because you will access your application with a browser on your machine (where the name is localhost, or 127.0.0.1), but inside Docker it will run in its own container, where the name is keycloak.

If you want to prove everything works, ensure everything is started at http://localhost:8500, then run npm run e2e -ws from the root project directory. This command will run the Cypress tests that JHipster generates in your browser.

Switch identity providers

JHipster ships with Keycloak when you choose OAuth 2.0 / OIDC as the authentication type. However, you can easily change it to another identity provider, like Auth0!

First, you’ll need to register a regular web application. Log in to your Auth0 account (or sign up if you don’t have an account). You should have a unique domain like dev-xxx.us.auth0.com.

Select Create Application in the Applications section. Use a name like Micro Frontends, select Regular Web Applications, and click Create.

Switch to the Settings tab and configure your application settings:

  • Allowed Callback URLs: http://localhost:8080/login/oauth2/code/oidc

  • Allowed Logout URLs: http://localhost:8080/

Scroll to the bottom and click Save Changes.

In the roles section, create new roles named ROLE_ADMIN and ROLE_USER.

Create a new user account in the users section. Click the Role tab to assign the roles you just created to the new account.

Make sure your new user’s email is verified before attempting to log in!

Next, head to Actions > Flows and select Login. Create a new action named Add Roles and use the default trigger and runtime. Change the onExecutePostLogin handler to be as follows:

exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://www.jhipster.tech';
  if (event.authorization) {
    api.idToken.setCustomClaim('preferred_username', event.user.email);
    api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
    api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
  }
}

This code adds the user’s roles to a custom claim (prefixed with https://www.jhipster.tech/roles). This claim is mapped to Spring Security authorities in SecurityUtils.java in the gateway app.

Select Deploy and drag the Add Roles action to your Login flow.

Edit docker-compose/central-server-config/application.yml and append the following YAML block to add your Auth0 settings.

jhipster:
  security:
    ...
    oauth2:
      audience: https://<your-auth0-domain>/api/v2/

spring:
  security:
    oauth2:
      client:
        provider:
          oidc:
            issuer-uri: https://<your-auth0-domain>/ # make sure to include the trailing slash!
        registration:
          oidc:
            client-id: <your-client-id>
            client-secret: <your-client-secret>
Want to have all these steps automated for you? Vote for issue #351 in the Auth0 CLI project.

Stop all your Docker containers with Ctrl+C and start them again.

docker compose up

Now, Spring Security will be configured to use Auth0, and Consul will distribute these settings to all your microservices. When everything is started, navigate to http://localhost:8080 and click sign in. You will be prompted for your Auth0 credentials.

Auth0 login

After entering your credentials, you’ll be redirected back to the gateway, and your username will be displayed.

Auth0 login success

You should be able to add, edit, and delete blogs, posts, tags, and products, proving that your microservices and micro frontends can talk to each other.

If you’d like to use Okta for your identity provider, see JHipster’s documentation.

You can configure JHipster quickly with the Okta CLI:

okta apps create jhipster

Deploy with Kubernetes

The JDL you used to generate this microservices stack has a section at the bottom for deploying to Kubernetes.

deployment {
  deploymentType kubernetes
  appsFolders [gateway, blog, store]
  clusteredDbApps [store]
  kubernetesNamespace demo
  kubernetesUseDynamicStorage true
  kubernetesStorageClassName ""
  serviceDiscoveryType consul
  dockerRepositoryName "mraible"
}

The jhipster jdl command generates a kubernetes directory with this information and configures all your apps, databases, and Consul to be Kubernetes-ready. If you have a Kubernetes cluster created, you can deploy to its demo namespace using the following command.

./kubectl-apply.sh -f

It also generates files for Kustomize and Skaffold if you’d prefer to use those tools. See the kubernetes/K8S-README.md file for more information.

I won’t go into the nitty-gritty details of deploying a JHipster microservices stack to cloud providers with K8s, mainly because it’s covered in previous blog posts. The first post below shows how to run Minikube locally, encrypt your secrets, and deploy to Google Cloud.

The JHipster team also has a blog you can follow at dev.to/jhipster.

Learn more about micro frontends and microservices

I hope you enjoyed this overview of how to use micro frontends within a Java microservices architecture. I like how micro frontends allow each microservice application to be self-contained and deployable, independent of the other microservices. It’s also pretty neat how JHipster generates Docker and Kubernetes configuration for you. Cloud-native FTW!

You can find the source code for this example on GitHub, in the @oktadev/auth0-micro-frontends-jhipster-example repository.

If you’d like to learn more about micro frontends and microservices, I recommend these posts:

Please follow @auth0 and @oktadev on Twitter. We also have YouTube channels that you might enjoy at youtube.com/auth0 and youtube.com/oktadev. If you have any questions, please leave a comment below!

Matt Raible is a well-known figure in the Java community and has been building web applications for most of his adult life. For over 20 years, he has helped developers learn and adopt open source frameworks and use them effectively. He's a web developer, Java Champion, and Developer Advocate at Okta. Matt has been a speaker at many conferences worldwide, including Devnexus, Devoxx Belgium, Devoxx France, Jfokus, and JavaOne. He is the author of The Angular Mini-Book, The JHipster Mini-Book, Spring Live, and contributed to Pro JSP. He is frequent contributor to open source and a member of the JHipster development team. You can find him online @mraible and raibledesigns.com.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.