This article is featured in the DZone Guide to the Java Ecosystem. Get your free copy for more insightful articles, industry statistics, and more.
- Strategies for building microservices in the Java ecosystem include container-less, self-contained, and in-container.
- Container-less microservices package the application, with all its dependencies, into a single “fat” JAR file.
- Self-contained microservices also package a single fat JAR, but these also include an embedded framework with optional third-party libraries that will be compatible.
- In-container microservices package an entire Java EE container and its service implementation in a Docker image.
Architectures based on microservices introduce new challenges for architects and developers. An ever-increasing list of languages and tools brings with it the capabilities to conquer this challenge. Java is no exception. This article explores different approaches to building microservices using the Java ecosystem.
This article does not discuss whether microservices are good or evil, nor whether you should design your app for microservices upfront or extract the services as they emerge from your monolith application.
The approaches described here are not the only ones available, but they should give you a pretty good overview of several possibilities. Even though the Java ecosystem is the main focus in this article, the concepts should be transferrable to other languages and technologies.
I have named the approaches in this article container-less, self-contained, and in-container. These terms may not be entirely established, but they fulfill their purpose here to differentiate the approaches. I will describe what each means in the sections that follow.
In the container-less approach, the developer treats everything on top of the JVM as a part of the application.
The container-less approach enables so-called single JAR deployment (also called a “fat JAR deployment”). This means that the application, with all its dependencies, is packaged as a single JAR file and can be run as a standalone Java process.
$ java -jar myservice.jar
One advantage of this approach is that it is extremely easy to start and stop services as needed when scaling up or down. Another advantage is convenient distribution. You just need to pass one JAR file around.
A downside of this approach is library compatibility. You are on your own for things like transaction support, or you need to bring in a third party library that provides support for this scenario. Later on, if you need support for something else, say persistence, you may need to fight compatibility issues between the libraries.
Another variant of single JAR deployment is building your services with an embedded framework. In this approach, the framework provides implementations of the services needed and the developer can choose which to include in the service.
You may argue that this is exactly the same as the container-less solution, but I like to distinguish them here since the self-contained approach actually gives you a set of third party libraries that you know are compatible.
This approach can involve tools like Spring Boot and Wildfly Swarm.
Spring Boot and the Spring Cloud Netflix projects have excellent support for building microservices in Java. Spring Boot allows you to pick and choose various parts of the Spring ecosystem, as well as popular external tools and then package them along with your application in a JAR file. Spring Initializr allows you to do this with a simple checkbox list form. A simple Hello World service is shown in this example: Gist Snippet
A Java EE counterpart to Spring Boot is WildFly Swarm. It enables you to pick and choose which parts of the Java EE specification you need and package them and your application in a JAR file. The Hello World example looks like this: Gist Snippet
The advantage of the self-contained approach is that you get to select only what you need in order for the service to run.
One disadvantage of this approach is that the configuration is a little more complex and the resulting deliverable JAR file is a bit bigger since it builds in the required container capabilities in the actual service.
While it seems like a lot of overhead to require an entire Java EE container to be able to deploy a microservice, keep in mind that some developers argue that the the ‘micro’ in microservice does not necessarily mean that the service is small or simple.
In these cases it may seem appropriate to treat the Java EE container as the required platform. Thus, the only dependency you need is the Java EE API. Note that the dependency is provided since the implementation is provided by the container. That means that the resulting WAR file is extremely lean. The implementation of the service is the same as the Wildfly Swarm example above. See it here: Gist Snippet
The advantage of this approach is that the container provides tested and verified implementations of standard functionality through standard APIs. Thus, you as a developer can focus entirely on the business functionality and leave the plumbing out of the application source.
Another advantage of this approach is that the actual application code does not depend on the Java EE application server it is deployed to, whether it is GlassFish, WildFly, WebLogic, WebSphere, or any other Java EE compatible implementations.
The disadvantage is that you need to deploy the service into a container and thus increase the complexity of the deployment.
This is where Docker comes in. By packaging the Java EE Container and the service implementation in a Docker image, you achieve more or less the same result as you would with a single JAR deployment. The difference is that now the service is contained in a Docker image and not a JAR file.
Dockerfile FROM jboss/wildfly:9.0.1.Final ADD myservice.war /opt/jboss/wildfly/standalone/deployments
The service is started by starting the Docker image in the Docker engine.
$ docker run -it -p 8081:8080 myorganization/myservice
The observant reader may have noticed the @EnableEurekaClient annotation in the Spring Boot code snippet from before. This annotation registers the service with Eureka, making it discoverable by service consumers. Eureka is a part of the Spring Cloud Netflix bundle and is an extremely easy-to-use and configure service discovery solution.
Java EE does not offer this functionality out of the box, but there are several open-source solutions available. One such solution is Snoop, which functions in a similar way to Eureka. The only thing needed to make a Java EE microservice available for service lookup is the @EnableSnoopClient annotation as shown in this example: Gist Snippet
Java is an excellent choice when building microservices. Any of the approaches described in this article will get things done. The most appropriate method for your particular case depends on the requirements of the service. For simpler services, a container-less or self-contained service is the better choice, but more advanced services may be faster and easier to implement with the power of an in-container implementation. Either way, Java is a proven ecosystem for implementing microservices.
For more insights on microservices, JVM languages, and more trends in Java, get your free copy of the DZone Guide to the Java Ecosystem!