Java microservices: architecture, best practices, tutorials

Java microservices: architecture, best practices, tutorials

Sep 21, 2020
Dmitry Chuyko

There is hardly any developer left who hasn’t heard about the microservices. So much is written about microservice architecture, and yet you might still stumble through the basics.

In this article, we give the definition of microservice architecture, discuss Java microservice frameworks, containers, native images, and the intricacies of creating and supporting microservices from a Java™ language perspective. You will find out how to turn your application into a working microservice. Intrigued? Dive right in!


What are microservices?

The microservice architecture is a lightweight method of organizing software as opposed to monoliths.

Previously, applications were mostly developed on a single codebase as centralized entities. Such an application had dozens of functionalities that processed tons of data. This in turn led to updating or troubleshooting being a difficult process: one tiny requirement made the team change the entire monolith.

However, this style of software development was great for its purposes until applications became more prevalent on mobile devices and the cloud. When your back-end data is on different and numerous devices, monolithic architecture won’t cut it.

This is where service-oriented architecture (SOA) comes in. SOA was an attempt to solve the downtime issue by dividing the structure into discrete units of similar functionality, i.e., services.

Microservices are a variant of SOAs. Such services were designed to avoid the risk of software bloat. Microservices are smaller in size and communicate over a network through language-agnostic protocols, like APIs. It gives developers more freedom in choosing production tools since they don’t have to rely on enterprise service buses, other services, and the way they couple. Then, thanks to advances in containerization, parts of an application with microservices have become even more autonomous. Now business components of your application can be controlled individually while running simultaneously on the same hardware. Microservices in containers give way to cloud-native computing along with efficient and scalable applications. And if you use tiny containers built upon the inherently lightweight Alpine Linux, you will be able to reduce deployment time and cloud expenses significantly.

Java Microservice Architecture

If your team already has a monolithic application that grew out of its shoes, you can split it into a system of microservices. With time you can add new services that take off some load off the existing ones.

Below you will find the tools that will help you to create microservice architecture in Java™.

Contexts and Dependency Injection

In Java EE, Contexts and Dependency Injection is a feature that helps to control the lifecycle of stateful objects using domain-specific contexts and integrate various components into client objects in a type-safe way.

Annotations or various means of external configuration make it possible to animate plain class fields. Containers or frameworks that have control over class/bean lifecycle can inject necessary fields after constructing them in a sophisticated manner. Original code will not be affected by that complexity and will not be disrupted by a replacement for testing or changing the way of communication. The underlying principle of such architecture is called Inversion of Control (IoC).

Dynamic proxies

When we create service architecture, the role of reflection is critical. It powers discovery mechanisms, and components/beans find each other in run time, taking only required actions. In Java™, we can create a facade instance that intercepts interface method calls. This design pattern is called a Dynamic Proxy and is made with the help of java.lang.reflect.Proxy and java.lang.reflect.InvocationHandler. This is a built-in reflection API mechanism, widely used in CDI containers, e.g., in Spring.

Web Server

Frameworks allow the developers to plug in tunable components on a high level. But don’t forget about the low level programming and a whole world of lower-level communication protocols like HTTP where we need an intermediary between network connections and business logic. There’s also a need to manage some state (contexts), resources, and security. That’s what web servers do. Each microservice is coupled with its server instance configured in a centralized manner with the framework’s help, like Embedded Tomcat.

Java Virtual Machine

JVM and the core class library serve as a foundation for the web server, libraries, and business logic. The developer’s bytecode is verified and executed by the runtime and may be easily examined by standard diagnostic tools like Mission Control.

JVM tuning is involved in the tuning of the web server, framework, libraries, and the application as well. As they typically allow customization, it’s natural to configure them all at once.

In addition, JVM may be updated independently of business logic. So security updates, new features, and optimized defaults go into production with the main code intact.


Containerization is a modern technology that makes all OS and external communications abstract without the downsides of hardware virtualization. Container management systems like Docker or Podman let us define, publish, and run container images with various software. Operating systems provide necessary levels of isolation and constrain resources for each container instance as requested by a management tool.

Software components in a container image form a stack. The software stack makes physical deployment of a microservice more effective. When top levels are updated, we may reuse cached lower ones. Thus, binaries from cached layers aren’t transferred, and other preliminary actions do not perform. A layered image looks like this:

The job of developers is to configure all these parts in such a way that they interact with one another, work correctly in a host OS, and communicate with external systems.

Finally, there are orchestration systems that make it possible to distribute containers over a cluster of machines, like Kubernetes and Marathon. Here, traces of Java™ code and JVM disappear, building blocks become the configured containers deployed over the server fleet, and they may coexist with SaaS components available in a cloud.

Benefits of microservices

There are multiple instances where developers might find microservices quite useful.

  1. Scalability. Microservices are a perfect technology if you plan to scale your application up or down (vertically with CPUs/memory), out or in (horizontally with nodes/desktops/servers). Such flexibility is granted by each service being independent. Plus, it is possible to alter the application dynamically: turning components on and off to balance computational loads.
  2. Cloud-native apps. Microservices go well together with Docker containers, where they are encapsulated along with runtimes, frameworks and libraries, even an operating system. This way, every business function is separated, maintained, and deployed in the cloud as one unit.
  3. Undemanding maintenance. Smaller services are easier to test and debug. The microservice architecture is very straightforward: here’s an interface, its implementation, and a defined number of services. You can replace some of them and don’t touch the rest. Combined with continuous delivery, this design model drives you closer to delivering fail-proof products to your customers.
  4. High resiliency. Since an application is decoupled into independent services, it’s more resistant to errors. A bug in one part of the code will not cause the entire system to shut down. Instead, the team will only have to fix a service.
  5. Overall economy. A benefit in three parts:
    • Building an application as individual services means short release cycles. An enterprise doesn’t need a complete redesign to modernize one service; it uses continuous delivery with cross-functional teams and deploys services independently.
    • Containerized images with microservices use file systems such as UnionFS, AuFS, OverlayFS. They create layers that describe how to recreate actions done to a base image. As a result, only the number of layer updates increases, leaving containers lightweight, saving valuable resources and reducing company expenses.
    • When functionalities are autonomous, you can assign separate teams to work on each. They don’t even have to be at the same place. In addition, your engineers will be able to deploy microservices with the simplest devices (read “x86 32 bit”) so you don’t need to equip yourself with pricey machines.
  6. A wide selection of tools. The microservice architecture helps to avoid vendor lock-in. If components don’t depend on one another, they may run on different frameworks or use a number of libraries. Developers might even choose several languages to write code for a single application.
  7. Independence from the database. When a service gets separated from the system, the database links get very flexible because of a unified interface. It is possible to make changes to the data or even replace the database entirely.

Java microservices frameworks

Frameworks are critical to implementing many routine tasks within microservices. They are very abundant in our industry, but they don’t exist in a vacuum and sometimes share common APIs. It is not uncommon for frameworks to support multiple APIs because they allow porting existing code as is or with minor changes. Below you will find the most useful frameworks for building microservices in Java, as well as sample code to launch your demo apps.

Spring Boot

Spring Boot, a part of an extensive Spring ecosystem, is the most popular framework in Java development. Spring Boot uses an embedded server Tomcat thus eliminating the need for Java EE containers. It also supports IoC, AOP, and has various projects for convenient microservices development, including

  • Spring Data for microservices associated with data access;
  • Spring Security for integrating authentication and authorization features;
  • Spring Cloud for building distributed systems.

In addition, the Spring ecosystem was recently enriched with Spring Native, a platform for native image construction. We will talk about the benefits of native images later, but for now, let’s create a sample microservice in Spring Boot.

Starting with Spring Boot couldn’t be more convenient. Use a spring initializr to create a web server. Select Maven Project, the latest Spring Boot version and the Java version you are using. Add Spring Web dependency. Finally, download the project, open it in your favorite IDE, and add the following sample code to the DemoApplication class:

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;



public class DemoApplication {

  public static void main(String[] args) {, args);



  public String hello() {

    return "Hi there!";



That’s it! You can launch the app, and it will listen on http://localhost:8080.


Eclipse Jersey is a Java framework aiding in RESTful Web Services development. Jersey implements the JAX-RS API and makes the creation of RESTful services a lot easier.

To get started with Jersey, study the documentation at the developer’s website. You can create a Maven project, run

mvn archetype:generate -DarchetypeArtifactId=jersey-quickstart-grizzly2 \

-DarchetypeGroupId=org.glassfish.jersey.archetypes -DinteractiveMode=false \

-DgroupId=com.example -DartifactId=simple-service -Dpackage=com.example \


This will generate a Jersey project on top of a Grizzly container, which you can mend as you see fit.


Micronaut is a freshman in the world of Java frameworks, but it was created especially for facilitating microservices development. It includes built-in cloud-native support and AOT compilation, and is compatible with several programming languages: Java, Kotlin, and Groovy.

To start with Micronaut, create a demo application at Then, create a HelloController class and add

import io.micronaut.http.MediaType;

import io.micronaut.http.annotation.Controller;

import io.micronaut.http.annotation.Get;

import io.micronaut.http.annotation.Produces;


public class HelloController {



   public String index() {

       return "Hello World!";



The application will answer with “Hello World!” at http://localhost:8080.

Microservices best practices

Microservice system can quickly spin out of control due to its complexity and turn into an intricate web that even senior developers will find hard to unweave. How to solve this problem? Decouple services with event streaming platforms (Kafka, Kinesis, Spark Streaming), simplify the service-to-service communication with a service mesh (Istio, OSM on Kubernetes), or redesign the system. Optimally, your goal is to avoid this:

…by turning it into this:

Note that the first scheme doesn’t feature any connection between message queues (represented by “MQ” cylinders) and databases.

Also, if we look at the upper level, right below the user, we can see that it retains substantial complexity. Don’t forget that it’s rare to have only one application copy within a service. When they are numerous, it is necessary to understand where to address requests in case of failure.

A service mesh network is meant to facilitate request delivery. Inside it, requests do not leave their individual infrastructure level, routed between microservices via proxies. Individual proxies here are called “sidecars” because they go alongside each service or application.

The sidecar proxy design pattern is also used outside of microservices. It eases the tracking and maintenance of applications by abstracting particular features away from the central architecture: interservice communication, security, and monitoring.

However, even when we have our microservices running, configured, and distributed, there is another issue that calls for our attention. How to minimize memory and time consumption? And here’s where we turn to the Native Image technology for a solution.

First, how does Native Image work? Developers change a service binary form to a native executable with the help of GraalVM Native Image technology. A mix of Graal compiler applied in AOT mode and SubstrateVM virtual machine promises instant startup time, low memory consumption, short disk space requirements, and excellent performance. The native image runs Java™ microservices in closed-world assumptions. Base container level must provide minimal functionality (and there’s no JDK), which means it may be a “scratch” base image. It will work if a deployed application brings its dependencies: e.g., a native image may be statically linked with a standard C library.

To facilitate the creation of native images, BellSoft developed a special tool, Liberica Native Image Kit. It allows creating microservices in different programming languages and supports a wide range of JDK versions and platforms so you can create an extensive network of microservices without worrying about compatibility.

Running your Java™ project in a native image is obviously a more attractive choice in performance and speed. However, picking either runtime or native image for building the microservice architecture depends on the tasks at hand, business environment, the state of your enterprise, and many other aspects.

Additional tutorials

If you’d like to put theory into practice, you can build your own application using our series of Java microservices tutorials:

We also prepared a set of tutorials on how to integrate Liberica Native Image Kit with popular Java frameworks:

Microservices have the status of a young but promising technology. If you feel that your company is ready to disrupt the status quo and break up its monolithic golem, give microservices a try. And if you need expert help our Java professionals are ready to consult you on the matter.

Subcribe to our newsletter


Read the industry news, receive solutions to your problems, and find the ways to save money.