Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon

Building microservices from a monolith Java EE app [Tutorial]

Save for later
  • 11 min read
  • 03 Aug 2018

article-image
Microservices are one of the top buzzwords these days. It's easy to understand why: in a growing software industry where the amount of services, data, and users increases crazily, we really need a way to build and deliver faster, decoupled, and scalable solutions.

In this tutorial, we'll help you get started with microservices or go deeper into your ongoing project.

This article is an extract from the book Java EE 8 Cookbook, authored by Elder Moraes.

One common question that I have heard dozens of times is, "how do I break down my monolith into microservices?", or, "how do I migrate from a monolith approach to microservices?"


Well, that's what this recipe is all about.

Getting ready with monolith and microservice projects


For both monolith and microservice projects, we will use the same dependency:

        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>8.0</version>
            <scope>provided</scope>
        </dependency>

Working with entities and beans


First, we need the entities that will represent the data kept by the application.

Here is the User entity:

@Entity
public class User implements Serializable {
private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column
private String name;

@Column
private String email;

public User(){ 
}

public User(String name, String email) {
this.name = name;
this.email = email;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
} 
}


Here is the UserAddress entity:

@Entity
public class UserAddress implements Serializable {
private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column
@ManyToOne
private User user;

@Column
private String street;

@Column
private String number;

@Column
private String city;

@Column
private String zip;

public UserAddress(){

}

public UserAddress(User user, String street, String number, 
String city, String zip) {
this.user = user;
this.street = street;
this.number = number;
this.city = city;
this.zip = zip;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}

public String getStreet() {
return street;
}

public void setStreet(String street) {
this.street = street;
}

public String getNumber() {
return number;
}

public void setNumber(String number) {
this.number = number;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public String getZip() {
return zip;
}

public void setZip(String zip) {
this.zip = zip;
}
}


Now we define one bean to deal with the transaction over each entity.

Here is the UserBean class:

@Stateless
public class UserBean {
@PersistenceContext
private EntityManager em;

public void add(User user) {
em.persist(user);
}

public void remove(User user) {
em.remove(user);
}

public void update(User user) {
em.merge(user);
}

public User findById(Long id) {
return em.find(User.class, id);
}

public List<User> get() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> pet = cq.from(User.class);
cq.select(pet);
TypedQuery<User> q = em.createQuery(cq);
return q.getResultList();
}

}


Here is the UserAddressBean class:

@Stateless
public class UserAddressBean {
@PersistenceContext
private EntityManager em;

public void add(UserAddress address){
em.persist(address);
}

public void remove(UserAddress address){
em.remove(address);
}

public void update(UserAddress address){
em.merge(address);
}

public UserAddress findById(Long id){
return em.find(UserAddress.class, id);
}

public List<UserAddress> get() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<UserAddress> cq = cb.createQuery(UserAddress.class);
Root<UserAddress> pet = cq.from(UserAddress.class);
cq.select(pet);
TypedQuery<UserAddress> q = em.createQuery(cq);
return q.getResultList();
} 
}


Finally, we build two services to perform the communication between the client and the beans.

Here is the UserService class:

@Path("userService")
public class UserService {
@EJB
private UserBean userBean;

@GET
@Path("findById/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response findById(@PathParam("id") Long id){
return Response.ok(userBean.findById(id)).build();
}

@GET
@Path("get")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response get(){
return Response.ok(userBean.get()).build();
}

@POST
@Path("add")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON) 
public Response add(User user){
userBean.add(user);
return Response.accepted().build();
}

@DELETE
@Path("remove/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON) 
public Response remove(@PathParam("id") Long id){
userBean.remove(userBean.findById(id));
return Response.accepted().build();
}
}


Here is the UserAddressService class:

@Path("userAddressService")
public class UserAddressService {
@EJB
private UserAddressBean userAddressBean;

@GET
@Path("findById/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response findById(@PathParam("id") Long id){
return Response.ok(userAddressBean.findById(id)).build();
}

@GET
@Path("get")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response get(){
return Response.ok(userAddressBean.get()).build();
}

@POST
@Path("add")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON) 
public Response add(UserAddress address){
userAddressBean.add(address);
return Response.accepted().build();
}

@DELETE
@Path("remove/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON) 
public Response remove(@PathParam("id") Long id){
userAddressBean.remove(userAddressBean.findById(id));
return Response.accepted().build();
}
}


Now let's break it down!

Building microservices from the monolith


Our monolith deals with User and UserAddress. So we will break it down into three microservices:

  • A user microservice
  • A user address microservice
  • Unlock access to the largest independent learning library in Tech for FREE!
    Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
    Renews at $19.99/month. Cancel anytime
  • A gateway microservice


A gateway service is an API between the application client and the services. Using it allows you to simplify this communication, also giving you the freedom of doing whatever you like with your services without breaking the API contracts (or at least minimizing it).

The user microservice


The User entity, UserBean, and UserService will remain exactly as they are in the monolith. Only now they will be delivered as a separated unit of deployment.

The user address microservice


The UserAddress classes will suffer just a single change from the monolith version, but keep their original APIs (that is great from the point of view of the client).

Here is the UserAddress entity:

@Entity
public class UserAddress implements Serializable {
private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

@Column
private Long idUser;

@Column
private String street;

@Column
private String number;

@Column
private String city;

@Column
private String zip;

public UserAddress(){

}

public UserAddress(Long user, String street, String number, 
String city, String zip) {
this.idUser = user;
this.street = street;
this.number = number;
this.city = city;
this.zip = zip;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public Long getIdUser() {
return idUser;
}

public void setIdUser(Long user) {
this.idUser = user;
}

public String getStreet() {
return street;
}

public void setStreet(String street) {
this.street = street;
}

public String getNumber() {
return number;
}

public void setNumber(String number) {
this.number = number;
}

public String getCity() {
return city;
}

public void setCity(String city) {
this.city = city;
}

public String getZip() {
return zip;
}

public void setZip(String zip) {
this.zip = zip;
}
}


Note that User is no longer a property/field in the UserAddress entity, but only a number (idUser). We will get into more details about it in the following section.

The gateway microservice


First, we create a class that helps us deal with the responses:

public class GatewayResponse {
private String response;
private String from;

public String getResponse() {
return response;
}

public void setResponse(String response) {
this.response = response;
}

public String getFrom() {
return from;
}

public void setFrom(String from) {
this.from = from;
}
}


Then, we create our gateway service:

@Consumes(MediaType.APPLICATION_JSON)
@Path("gatewayResource")
@RequestScoped
public class GatewayResource {
private final String hostURI = "http://localhost:8080/";
private Client client;
private WebTarget targetUser;
private WebTarget targetAddress;

@PostConstruct
public void init() {
client = ClientBuilder.newClient();
targetUser = client.target(hostURI + 
"ch08-micro_x_mono-micro-user/");
targetAddress = client.target(hostURI +
"ch08-micro_x_mono-micro-address/");
}

@PreDestroy
public void destroy(){
client.close();
}

@GET
@Path("getUsers")
@Produces(MediaType.APPLICATION_JSON)
public Response getUsers() {
WebTarget service = 
targetUser.path("webresources/userService/get");

Response response;
try {
response = service.request().get();
} catch (ProcessingException e) {
return Response.status(408).build();
}

GatewayResponse gatewayResponse = new GatewayResponse();
gatewayResponse.setResponse(response.readEntity(String.class));
gatewayResponse.setFrom(targetUser.getUri().toString());

return Response.ok(gatewayResponse).build();
}

@POST
@Path("addAddress")
@Produces(MediaType.APPLICATION_JSON) 
public Response addAddress(UserAddress address) {
WebTarget service = 
targetAddress.path("webresources/userAddressService/add");

Response response;
try {
response = service.request().post(Entity.json(address));
} catch (ProcessingException e) {
return Response.status(408).build();
}

return Response.fromResponse(response).build();
}

}


As we receive the UserAddress entity in the gateway, we have to have a version of it in the gateway project too. For brevity, we will omit the code, as it is the same as in the UserAddress project.

Transformation to microservices


The monolith application couldn't be simpler: just a project with two services using two beans to manage two entities.

The microservices


So we split the monolith into three projects (microservices): the user service, the user address service, and the gateway service.

The user service classes remained unchanged after the migration from the monolith version. So there's nothing to comment on.

The UserAddress class had to be changed to become a microservice. The first change was made on the entity.

Here is the monolith version:

@Entity
public class UserAddress implements Serializable {
...

@Column
@ManyToOne
private User user;

...

public UserAddress(User user, String street, String number, 
String city, String zip) {
this.user = user;
this.street = street;
this.number = number;
this.city = city;
this.zip = zip;
}

...

public User getUser() {
return user;
}

public void setUser(User user) {
this.user = user;
}

...

}


Here is the microservice version:

@Entity
public class UserAddress implements Serializable {
...

@Column
private Long idUser;

...

public UserAddress(Long user, String street, String number, 
String city, String zip) {
this.idUser = user;
this.street = street;
this.number = number;
this.city = city;
this.zip = zip;
}

public Long getIdUser() {
return idUser;
}

public void setIdUser(Long user) {
this.idUser = user;
}

...

}


Note that in the monolith version, user was an instance of the User entity:

private User user;


In the microservice version, it became a number:

private Long idUser;


This happened for two main reasons:

  1. In the monolith, we have the two tables in the same database (User and UserAddress), and they both have physical and logical relationships (foreign key). So it makes sense to also keep the relationship between both the objects.
  2. The microservice should have its own database, completely independent from the other services. So we choose to keep only the user ID, as it is enough to load the address properly anytime the client needs.


This change also resulted in a change in the constructor.

Here is the monolith version:

public UserAddress(User user, String street, String number, 
                   String city, String zip)


Here is the microservice version:

public UserAddress(Long user, String street, String number, 
                   String city, String zip)


This could lead to a change of contract with the client regarding the change of the constructor signature. But thanks to the way it was built, it wasn't necessary.

Here is the monolith version:

public Response add(UserAddress address)


Here is the microservice version:

public Response add(UserAddress address)


Even if the method is changed, it could easily be solved with @Path annotation, or if we really need to change the client, it would be only the method name and not the parameters (which used to be more painful).

Finally, we have the gateway service, which is our implementation of the API gateway design pattern. Basically it is the one single point to access the other services.

The nice thing about it is that your client doesn't need to care about whether the other services changed the URL, the signature, or even whether they are available. The gateway will take care of them.

The bad part is that it is also on a single point of failure. Or, in other words, without the gateway, all services are unreachable. But you can deal with it using a cluster, for example.

So now you've built a microservice in Java EE code, that was once a monolith! If you found this tutorial helpful and would like to learn more, head over to this book Java EE 8 Cookbook, authored by Elder Moraes.

Oracle announces a new pricing structure for Java

Design a RESTful web API with Java [Tutorial]

How to convert Java code into Kotlin