Microservices with JHipster

Adopting a microservices architecture provides unique opportunities to add failover and resiliency to your systems, so your components can handle load spikes and errors gracefully. Microservices make change less expensive, too. They can also be a good idea when you have a large team working on a single product. You can break up your project into components that can function independently. Once components can function independently, they can be built, tested, and deployed independently. This gives an organization and its teams the agility to develop and deploy quickly.

Before we dive into the code tutorial, I’d like to talk about microservices, their history, and why you should (or should not) consider a microservices architecture for your next project.

History of microservices

According to Wikipedia, the term "microservice" was first used as a common architecture style at a workshop of software architects near Venice in May 2011. In May 2012, the same group decided "microservices" was a more appropriate name.

Adrian Cockcroft, who was at Netflix then, described this architecture as "fine-grained SOA". Martin Fowler and James Lewis wrote an article titled “Microservices” on March 25, 2014. Years later, this is still considered the definitive article for microservices.

Organizations and Conway’s law

Technology has traditionally been organized into technology layers: UI team, database team, operations team, etc. When teams are separated along these lines, even simple changes can lead to a cross-team project consuming huge chunks of time and budget.

A smart team will optimize around this and choose the lesser of two evils: forcing the logic into whichever application they have access to. This is an example of Conway’s law in action.

Conway’s law
Figure 1. Conway’s law
Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization’s communication structure.
— Melvyn Conway
1967

Microservices architecture philosophy

The philosophy of a microservices architecture essentially equals the Unix philosophy of "do one thing and do it well". The characteristics of a microservices architecture are as follows:

  • componentization via services

  • organized around business capabilities

  • products not projects

  • smart endpoints and dumb pipes

  • decentralized governance

  • decentralized data management

  • infrastructure automation

  • design for failure

  • evolutionary design

Why microservices?

For most developers, dev teams, and organizations, it’s easier to work on small "do one thing well" services. No single program represents the whole application, so services can change frameworks (or even languages) without a massive cost. As long as the services use a language-agnostic protocol (HTTP or lightweight messaging), you can write applications in several different platforms—Java, Ruby, Node, Go, .NET, etc.—without issue.

Platform-as-a-Service (PaaS) providers and containers have made it easy to deploy microservices. All the technologies needed to support a monolith (e.g., load balancing, discovery, process monitoring) are provided by the PaaS outside of your container. Deployment effort comes close to zero.

Are microservices the future?

The consequences of architecture decisions, like adopting microservices, usually only become evident several years after you make them. Microservices have been successful at companies like LinkedIn, Twitter, Facebook, Amazon.com, and Netflix, but that doesn’t mean they’ll be successful for your organization. Component boundaries are hard to define. If you’re unable to create your components cleanly, you’re just shifting the complexity from inside a component to the connections between components. Also, team capabilities are something to consider. A weak team will always create a weak system.

You shouldn’t start with a microservices architecture. Instead, begin with a monolith, keep it modular, and split it into microservices once the monolith becomes a problem.
— Martin Fowler

Reactive Java microservices

Spring Boot 2.0 introduced a new web framework called Spring WebFlux. Previous versions of Spring Boot only shipped with Spring MVC as an option. WebFlux offers a way for developers to do reactive programming. This means you can write your code with familiar syntax, and, as a result, your app will use fewer resources and scale better.

Spring Boot 2.0
Figure 2. Spring Boot 2.0

Reactive programming isn’t for every app. The general rule of thumb is it won’t help you if you have < 500 requests/second. Chances are Spring MVC will perform as well as Spring WebFlux up to that point. When your traffic takes off, or if you need to process things faster than 500 requests/second, you should look at Spring WebFlux.

JHipster 7 introduced support for Spring WebFlux. This means you can generate a reactive microservices architecture with Spring Cloud Gateway and Spring Boot quickly and easily. This is a great way to get started with reactive programming.

Spring WebFlux’s API has a similar syntax to Spring MVC. For example, here’s the Spring MVC code for creating a new Points entity in a JHipster app created with jhipster jdl 21-points.jh.

@PostMapping("/points")
public ResponseEntity<Points> createPoints(@Valid @RequestBody Points points) throws URISyntaxException {
    log.debug("REST request to save Points : {}", points);
    if (points.getId() != null) {
        throw new BadRequestAlertException("A new points cannot already have an ID", ENTITY_NAME, "idexists");
    }
    Points result = pointsRepository.save(points);
    pointsSearchRepository.index(result);
    return ResponseEntity
        .created(new URI("/api/points/" + result.getId()))
        .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.getId().toString()))
        .body(result);
}

The same functionality when implemented with Spring WebFlux returns a Mono and uses a more functional, streaming style to avoid blocking.

@PostMapping("/points")
public Mono<ResponseEntity<Points>> createPoints(@Valid @RequestBody Points points) throws URISyntaxException {
    log.debug("REST request to save Points : {}", points);
    if (points.getId() != null) {
        throw new BadRequestAlertException("A new points cannot already have an ID", ENTITY_NAME, "idexists");
    }
    return pointsRepository
        .save(points)
        .flatMap(pointsSearchRepository::save)
        .map(result -> {
            try {
                return ResponseEntity
                    .created(new URI("/api/points/" + result.getId()))
                    .headers(HeaderUtil.createEntityCreationAlert(applicationName, true, ENTITY_NAME, result.getId().toString()))
                    .body(result);
            } catch (URISyntaxException e) {
                throw new RuntimeException(e);
            }
        });
}

The code above was created by running jhipster jdl 21-points.jh --reactive.

Microservices with JHipster

In this example, I’ll show you how to build a reactive microservices architecture with JHipster. As part of this process, you’ll generate three applications and run several others.

  • Generate a gateway.

  • Generate a blog microservice.

  • Generate a store microservice.

  • Run Consul, Keycloak, Neo4j, and MongoDB.

Introducing Micro Frontends

Before JHipster 7.9.0, if you generated a microservices architecture with a UI, the gateway would be a monolithic UI. This means the gateway would contain all the Angular, React, or Vue files. This creates a tight coupling between the gateway and the microservices it routes to. If you want to change the UI for a microservice, you must also redeploy the gateway. This is a problem because you should be able to deploy your microservices independently.

You can solve this problem with micro frontends. Micro frontends are a way to break up your UI into smaller, independent pieces. JHipster added support for micro-frontends in 7.9.0. Microfrontends provide a way 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!

In the previous paragraph, 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 chapter since that’s what Cam Jackson used in his Micro Frontends article on Martin Fowler’s blog.

You can see how these components fit in the diagram below.

JHipster microservices architecture
Figure 3. JHipster microservices architecture

This tutorial shows you how to build a microservices architecture with JHipster 7.9.3. You’ll generate a gateway (powered by Spring Cloud Gateway), a blog microservice (that talks to Neo4j), and a store microservice (that uses MongoDB). The gateway will contain a React shell app that loads the blog and store micro frontends. You’ll use Docker Compose to make sure it all runs locally. I’ll also provide some pointers on how to deploy it with Kubernetes.

Generate an API gateway and microservice applications

Open a terminal window, create a directory (e.g., jhipster-microservices-example), and create an apps.jdl file in it. Copy the JDL below into this file. You can also download this file from GitHub.

Example 1. apps.jdl
application {
  config {
    baseName gateway
    reactive true (1)
    packageName com.okta.developer.gateway
    applicationType gateway
    authenticationType oauth2 (2)
    buildTool gradle
    clientFramework react (3)
    prodDatabaseType postgresql
    serviceDiscoveryType consul (4)
    testFrameworks [cypress] (5)
    microfrontends [blog, store] (6)
  }
}

application {
  config {
    baseName blog
    reactive true
    packageName com.okta.developer.blog
    applicationType microservice (7)
    authenticationType oauth2 (8)
    buildTool gradle
    clientFramework react (9)
    databaseType neo4j (10)
    devDatabaseType neo4j
    prodDatabaseType neo4j
    enableHibernateCache false
    serverPort 8081 (11)
    serviceDiscoveryType consul
    testFrameworks [cypress] (12)
  }
  entities Blog, Post, Tag
}

application {
  config {
    baseName store
    reactive true
    packageName com.okta.developer.store
    applicationType microservice
    authenticationType oauth2
    buildTool gradle
    clientFramework react
    databaseType mongodb (13)
    devDatabaseType mongodb
    prodDatabaseType mongodb
    enableHibernateCache false
    serverPort 8082
    serviceDiscoveryType consul
    testFrameworks [cypress]
  }
  entities Product
}

(14)
entity Blog {
  name String required minlength(3)
  handle String required minlength(2)
}

entity Post {
  title String required
  content TextBlob required
  date Instant required
}

entity Tag {
  name String required minlength(2)
}

entity Product {
  title String required
  price BigDecimal required min(0)
  image ImageBlob
}

(15)
relationship ManyToOne {
  Blog{user(login)} to User
  Post{blog(name)} to Blog
}

relationship ManyToMany {
  Post{tag(name)} to Tag{post}
}

(16)
paginate Post, Tag with infinite-scroll
paginate Product with pagination

(17)
deployment {
  deploymentType docker-compose
  serviceDiscoveryType consul
  appsFolders [gateway, blog, store]
  dockerRepositoryName "mraible"
}

(18)
deployment {
  deploymentType kubernetes
  appsFolders [gateway, blog, store]
  clusteredDbApps [store]
  kubernetesNamespace demo
  kubernetesUseDynamicStorage true
  kubernetesStorageClassName ""
  serviceDiscoveryType consul
  dockerRepositoryName "mraible"
}
1 Enable reactive support. You cannot set this to false for a gateway. This is because Spring Cloud Gateway is reactive-only. There is an open issue for Spring MVC support.
2 The authentication type for the gateway is OAuth 2.0.
3 The client framework used is React.
4 You must specify consul as the service discovery type for the gateway and all microservice apps. You can also use eureka, but I prefer consul because it’ll be the default in JHipster 8.
5 Including Cypress allows you to test the UI with npm run e2e.
6 Micro frontends are enabled for the gateway, and entities will be pulled in from the blog and store microservices.
7 For the microservice apps, you need to specify an application type of microservice.
8 The microservice app’s authentication type must match the gateway.
9 The client framework must be the same for all apps.
10 The blog app uses Neo4j as its database. You must use the same databases for dev and prod when using NoSQL options.
11 The default server port is 8080. You must specify different ports for each app.
12 If you want to test the UI of your micro frontend, you need to include Cypress.
13 The store app uses MongoDB for its database.
14 Entity definitions live outside your application definitions. You can validate your JDL using JDL-Studio or the JHipster JDL Plugin.
15 Relationships between entities can be defined in JDL!
16 If you want pagination on your list screens, you can use infinite scrolling or page links.
17 Creates Docker Compose files for all apps and a docker-compose.yml file that will start them.
18 Creates Kubernetes manifests for all apps and scripts to deploy them.

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.

Run JHipster’s jdl command to import this microservices architecture definition.

jhipster jdl apps.jdl --monorepository --workspaces

The project generation process will take a minute or two, depending on your internet connection speed and hardware.

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 similar to 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 microservices architecture

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.

JHipster microservices architecture
Figure 4. 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
Figure 5. Consul services

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

Consul services
Figure 6. 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 micro frontends. 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
Figure 7. 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 kbd:[Cmd+Tab] back to your browser.

Store edit
Figure 8. 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 logging 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:

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>/
        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 kbd:[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
Figure 9. Auth0 login

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

Auth0 login success
Figure 10. 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 other guides. The first one below shows how to run Minikube locally, encrypt your secrets, and deploy to Google Cloud.

Source code

You can find the source code for this microservices example at @oktadev/auth0-micro-frontends-jhipster-example.

Summary

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 neat how JHipster generates Docker and Kubernetes configurations for you. Cloud-native FTW!

Just because JHipster makes microservices easy doesn’t mean you should use them. Using a microservices architecture is a great way to scale development teams, but if you don’t have a large team, a “Majestic Monolith” might work better.