Scale-Up and Load-Balance a Spring-Boot MicroService With Docker-Compose, Persist Data and Enable Netflix Tools Like EUREKA and ZUUL API Gateway (PART 1)

Vutlhari Ndlovhu
The Startup
Published in
10 min readNov 13, 2020
Please look at the overview image of what we are going to accomplish today

Introduction

An amazing, amazing article this is. The power of having different docker image containers talk to each other in a seamless way is amazing. In this article, that’s exactly what we’ll be doing. We going to access a scaled-up CRUD(create, read, update and delete) REST API microservice via an API gateway and store its data on a PostgreSQL database server. We will have 4 docker images at the end of this article. One for PostgreSQL, one for REST API microservice, one for EUREKA to enable round-robin load-balancing, and the last one for ZUUL API gateway. To easily manage the containers we’ll use docker-compose. The journey to a super full stack developer takes one article at a time.

Prerequisite

Please read the “Custom Postgres Docker Image with predefined Database and Tables with permanent storage” first to be able to complete this part one and part 2 article. Find the link below

Create 3 projects to start our journey

  1. Create EUREKA Server Project

To start from scratch, you need to head over to the below URL:

https://start.spring.io/

And edit the group and artifact and add the dependencies needed by the project. Also, change the java version to 8. Please look at the image below and make sure you have similar dependencies

Dependencies explained:

Eureka Server — Enable our project to be a EUREKA server, which will, in turn, discover all the other projects

Spring Boot Actuator — To enable our project to be monitored by the EUREKA server and any other monitoring tools will be able to ping this service’s uptime.

After generating the project and unzipping it. Edit the Spring Boot Application class: SuperdevEurekaServerApplication

package za.co.superdev.discovery;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;@SpringBootApplication@EnableEurekaServerpublic class SuperdevEurekaServerApplication {public static void main(String[] args) {SpringApplication.run(SuperdevEurekaServerApplication.class, args);}}

And the last thing to edit is application.property

eureka.client.registerWithEureka = falseeureka.client.fetchRegistry = falseserver.port = 8761spring.application.name=superdev-eureka-server

Now you can start the Netflix EUREKA Server and check what is on the dashboard. Go to your terminal/command prompt and navigate to the parent folder of EUREKA project, make sure the folder has the pom.xml file, or else the following command won’t work.

mvn clean spring-boot:run

Then on your browser type the following URL:

http://localhost:8761/

You should see the EUREKA Server Dashboard page like below:

We are done with the first project, on to the next one

You can find the complete code for the EUREKA Server on the below GitHub link

https://github.com/vhutie/superdev-eureka-server.git

2. Create a JPA Spring boot project

To start from scratch, you need to head over to the below URL:

https://start.spring.io/

And edit the group and artifact and add the dependencies needed by the project. Also, change the java version to 8. Please look at the image below and make sure you have similar dependencies

Dependencies explained:

Spring Data JPA — We need it to assist us with the seamless persistence of data from java to any connected DB without doing the traditional connection or session setup and management via annotations.

Spring Web — We need it to enable REST API on our project, exposing web services in a seamless way via annotations.

H2 Database — Enable our project to communicate with an H2 Database, connecting and managing data in between.

Eureka Discovery Client — Enable our project to be discovered by a Eureka server and allow our services to communicate with this service in a seamless way.

Spring Boot Actuator — To enable our project to be monitored by EUREKA server and any other monitoring tools will be able to ping this service’s uptime.

Once all is done, click generate and the project will be automatically downloaded to your download folder. Then move it to your preferred location.

Then Open the project with your favorite IDE and check if all is in order as in the image below:

Let’s quickly edit the project to have at least a working rest api jpa enabled project.

We will need the following classes:

POJO(Plain Old Java Object) Entity Class to have data to object mappings via annotations

package za.co.superdev.crm.model;import java.util.Date;import javax.persistence.Entity;import javax.persistence.GeneratedValue;import javax.persistence.GenerationType;import javax.persistence.Id;@Entitypublic class Customer {@Id@GeneratedValue(strategy=GenerationType.AUTO)private Long id;private String name;private String surname;private Integer age;private Date creationDate;private Date lastUpdateDate;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 getSurname() {return surname;}public void setSurname(String surname) {this.surname = surname;}public Integer getAge() {return age;}public void setAge(Integer age) {this.age = age;}public Date getCreationDate() {return creationDate;}public void setCreationDate(Date creationDate) {this.creationDate = creationDate;}public Date getLastUpdateDate() {return lastUpdateDate;}public void setLastUpdateDate(Date lastUpdateDate) {this.lastUpdateDate = lastUpdateDate;}@Overridepublic String toString() {return String.format("Customer[id=%d, Name='%s', Surname='%s', Age='%s']",id, name, surname, age);}}

JPA Repository Interface handles the communication between our entity class and database. On this interface, you are able to define JPA queries and Native Queries.

package za.co.superdev.crm.repo;import java.util.List;import org.springframework.data.repository.CrudRepository;import za.co.superdev.crm.model.Customer;public interface CustomerRepository extends CrudRepository<Customer, Long> {List<Customer> findByName(String name);Customer findById(long id);}

Service Class is a Service annotated class which we use for any logic of how the database can store, retrieve, update, and delete data.

package za.co.superdev.crm.service;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import za.co.superdev.crm.model.Customer;import za.co.superdev.crm.repo.CustomerRepository;import java.util.Date;import java.util.List;import java.util.Objects;import java.util.Optional;import org.slf4j.Logger;import org.slf4j.LoggerFactory;@Servicepublic class CustomerService {private static final Logger logger = LoggerFactory.getLogger(CustomerService.class);@Autowiredprivate CustomerRepository customerRepository;public Customer create(Customer customer) throws Exception{try{customer.setCreationDate(new Date());Customer customerSaved = customerRepository.save(customer);return customerSaved;} catch(Exception e ) {logger.error("Exception occured Cause {} Message {} exception {}",e.getCause(), e.getMessage(), e);throw new Exception("The resource you were trying to reach is not found");}}public Customer findById(Long customerId) throws Exception{try{Optional<Customer> customerOptional = customerRepository.findById(customerId);if(customerOptional.isPresent())return customerOptional.get();elsethrow new Exception("The resource you were trying to reach is not found");} catch(Exception e ) {logger.error("Exception occured Cause {} Message {} exception {}",e.getCause(), e.getMessage(), e);throw new Exception("The resource you were trying to reach is not found");}}public List<Customer> findByName(String customerName) throws Exception{try{List<Customer> customerList = customerRepository.findByName(customerName);if(!Objects.isNull(customerList))return customerList;elsethrow new Exception("The resource you were trying to reach is not found");} catch(Exception e ) {logger.error("Exception occured Cause {} Message {} exception {}",e.getCause(), e.getMessage(), e);throw new Exception("The resource you were trying to reach is not found");}}public List<Customer> findAll() throws Exception{try{List<Customer> customerList = customerRepository.findAll();if(!Objects.isNull(customerList))return customerList;elsethrow new Exception("The resource you were trying to reach is not found");} catch(Exception e ) {logger.error("Exception occured Cause {} Message {} exception {}",e.getCause(), e.getMessage(), e);throw new Exception("The resource you were trying to reach is not found");}}}

Controller class, with this class we can expose our JPA project to a REST API web service

package za.co.superdev.crm.controller;import java.util.List;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.CrossOrigin;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestBody;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.bind.annotation.RestController;import za.co.superdev.crm.model.Customer;import za.co.superdev.crm.service.CustomerService;@RestController@RequestMapping("v1/api/customer")public class CustomerController {private static final Logger LOGGER = LoggerFactory.getLogger(CustomerController.class);@Autowiredprivate CustomerService customerService;@CrossOrigin(origins = "*")@PostMappingpublic @ResponseBody ResponseEntity<Object> createUser(@RequestBody Customer customer) {try {Customer response = customerService.create(customer);return new ResponseEntity<>(response, HttpStatus.OK);}catch (Exception e) {for (StackTraceElement ste : e.getStackTrace()) {LOGGER.error(ste.toString());}LOGGER.error(e.getMessage());return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);}}@CrossOrigin(origins = "*")@GetMapping("/{customerId}")public @ResponseBody ResponseEntity<Object> getById(@PathVariable(name = "customerId", required = true) Long customerId) {try {Customer response = customerService.findById(customerId);return new ResponseEntity<>(response, HttpStatus.OK);}catch (Exception e) {for (StackTraceElement ste : e.getStackTrace()) {LOGGER.error(ste.toString());}LOGGER.error(e.getMessage());return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);}}@CrossOrigin(origins = "*")@GetMapping("/extended/{customerName}")public @ResponseBody ResponseEntity<Object> getByName(@PathVariable(name = "customerName", required = true) String customerName) {try {List<Customer> response = customerService.findByName(customerName);return new ResponseEntity<>(response, HttpStatus.OK);}catch (Exception e) {for (StackTraceElement ste : e.getStackTrace()) {LOGGER.error(ste.toString());}LOGGER.error(e.getMessage());return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);}}@CrossOrigin(origins = "*")@GetMappingpublic @ResponseBody ResponseEntity<Object> getAll() {try {List<Customer> response = customerService.findAll();return new ResponseEntity<>(response, HttpStatus.OK);}catch (Exception e) {for (StackTraceElement ste : e.getStackTrace()) {LOGGER.error(ste.toString());}LOGGER.error(e.getMessage());return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);}}}

Edit application.properties

spring.datasource.url=jdbc:h2:mem:testdbspring.datasource.driverClassName=org.h2.Driverspring.datasource.username=saspring.datasource.password=passwordspring.jpa.database-platform=org.hibernate.dialect.H2Dialect#EDIT TO COMPUTER IP ADDRESSeureka.client.serviceUrl.defaultZone=${EUREKA_SERVER:http://localhost:8761/eureka}spring.application.name=superdev-crm-customer

Your project structure should look something like the picture below:

Final Project Structure

Now let's start your project and check if EUREKA has discovered it

Go to your terminal/command prompt and navigate to the parent folder of the Customer project, make sure the folder has the pom.xml file, or else the following command won’t work.

mvn clean spring-boot:run

Then on your browser type the following URL Or Refresh:

http://localhost:8761/

You should see your customer service on EUREKA Server Dashboard page like below:

You can also add a customer with Postman using the POST operation and remember to change your Request Body type to JSON.

You can also test getting all customers by using postman using the GET operation

You can find the complete code for the Customer Microservice on the below GitHub link

https://github.com/vhutie/superdev-crm-customer.git

3. Create ZUUL API Gateway Project, I promise this next one will be quick :-)

To start from scratch, you need to head over to the below URL:

https://start.spring.io/

And edit the group and artifact and add the dependencies needed by the project. Also, change java version to 8 and Spring boot version to 2.3.6 cause 2.4 wasn't working at the time this article was written. Please look at the image below and make sure you have similar dependencies.

Dependencies explained:

ZUUL — Enable our project to be a ZUUL API gateway

Eureka Discovery Client — Enable our project to be discovered by a Eureka server and allow our services to communicate with this service in a seamless way.

Spring Boot Actuator — To enable our project to be monitored by EUREKA server and any other monitoring tools will be able to ping this service’s uptime.

After generating the project and unzipping it. Edit the Spring Boot Application class: SuperdevZuulApplication

package za.co.superdev.api;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.cloud.client.discovery.EnableDiscoveryClient;import org.springframework.cloud.netflix.zuul.EnableZuulProxy;import org.springframework.context.annotation.Bean;@EnableZuulProxy@EnableDiscoveryClient@SpringBootApplicationpublic class SuperdevZuulApplication {public static void main(String[] args) {SpringApplication.run(SuperdevZuulApplication.class, args);}@Beanpublic SimpleFilter simpleFilter() {return new SimpleFilter();}}

Add a filter class for ZUUL

package za.co.superdev.api;import javax.servlet.http.HttpServletRequest;import com.netflix.zuul.context.RequestContext;import com.netflix.zuul.ZuulFilter;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class SimpleFilter extends ZuulFilter {private static Logger log = LoggerFactory.getLogger(SimpleFilter.class);@Overridepublic String filterType() {return "pre";}@Overridepublic int filterOrder() {return 1;}@Overridepublic boolean shouldFilter() {return true;}@Overridepublic Object run() {RequestContext ctx = RequestContext.getCurrentContext();HttpServletRequest request = ctx.getRequest();log.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));return null;}}

And edit the application.properties

server.port=8081#EDIT TO COMPUTER IP ADDRESSeureka.client.serviceUrl.defaultZone=${EUREKA_SERVER:http://localhost:8761/eureka}spring.application.name=superdev-zuulzuul.routes.customer.url=http://localhost:8080ribbon.eureka.enabled=false

application.properties explained:

server.port — we are assigning a different port from the default 8080 tomcat server

zuul.routes — we are mapping ZUUL to our customer service, any request that starts with a http://localhost:8081/customer will redirect to the customer’s service

ribbon.eureka.enabled — at this point we disabled the load balancing

Final project structure:

Now you can start the Netflix ZUUL API Gateway and check what is on the EUREKA dashboard. Go to your terminal/command prompt and navigate to the parent folder of ZUUL project, make sure the folder has the pom.xml file, or else the following command won’t work.

mvn clean spring-boot:run

Then on your browser type the following URL:

http://localhost:8761/

You should see ZUUL on the EUREKA Server Dashboard page like below:

You can test your ZUUL API Gateway by trying to access customer data via ZUUL API to get all Customers’ GET request on Postman

http://localhost:8081/customer/v1/api/customer

You can find the complete code for the ZUUL API Gateway on the below GitHub link

https://github.com/vhutie/superdev-zuul.git

Conclusion

Part 1 of this article has finally come to an end, let’s take it step by step and make sure we have done all the necessary steps. The following article depends on this article being complete.

Part 2 will focus on changing these projects to be docker enabled, adding them to a docker-compose file, changing the customer service to persist its information from the H2 database to Postgresql, and scaling up and down of the customer service. It will be amazing 😉

Share, like and teach others…

--

--

Vutlhari Ndlovhu
The Startup

Full Stack developer for over 15 years, I build Mobile Apps, Web Apps, Microservice, Middleware Integration, Workflow Engine, ETL and More...