Reduce your TCO of building and using Spring Native applications with just 3 steps. Switch to the best Unified Java Runtime. Learn More.

Containerizing Spring Boot apps with Docker

Creating Java microservices with Spring Boot and Liberica JDK


Published June 20, 2022


BellSoft Blog Disclaimer

A guide to building Java microservices. Part 1

In modern enterprise software development, microservices are becoming increasingly popular. Although this type of architecture is no silver bullet and implementing it is significantly more challenging compared to monoliths, it is the preferred one in many instances. For a deep dive into the theory of microservices, go to our previous article, introduction to understanding the architecture.

This post, in contrast, will deal with practical issues. Here I will describe designing and developing microservice architecture through a real-world use case: a cloud-native Spring Boot Java application for an online store based on open source Liberica JDK.

Are you currently working to create a network of microservices? We are here to help you with this exciting and challenging process, especially with everything Java-related. Fill the form and discuss developing your Java software with our senior engineer.

  1. Set up the project
    1. Unified Tech Stack
    2. Implementation
  2. Configure the Order microservice
    1. Domain/Entity
    2. Repository
    3. Controllers
  3. Configure the Customer microservice
    1. Services
  4. Conclusion

Set up the project

Unified Tech Stack

I’ll take a JVM-based tech stack for its legendary backward compatibility and being the number one choice in enterprise software development. Stable and reliable Java™ will suit best as a programming language for our purposes. What about the runtime? Oracle has recently introduced a hefty price for Oracle’s JDK use in production. Fortunately, several companies are offering OpenJDK (which is completely free) with commercial licensing. BellSoft Liberica JDK is one of the industry’s leading OpenJDK distributions, 100% Java SE compatible, and supported by an active contributor to this project.

As for the backend, several excellent Java-based enterprise software development frameworks are prominent among the community. Spring from Pivotal, MicroProfile from the Eclipse Foundation, Quarkus from Red Hat, and Micronaut from Object Computing Inc. are the dominant ones in this field.

Among them, I will pick Spring since it has been the primary native Java-based enterprise software development framework for the last 20 years and is still going strong in the age of cloud computing. An additional advantage is that Liberica JDK is the default runtime in Spring, which makes our stack of choice uniform and strong.

Implementation

Developing a full-blown e-commerce app will be too much for a blog post. So we’ll create a simplified store based on microservice architecture. This is going to be a headless backend application without a frontend. However, testing is possible with any REST API client, e.g., by using CURL or POSTMAN.

We will create Customer and Order microservices, each of them will handle one specific task according to the general principle of microservice architecture.

First, we need to create a Spring Boot project with dependencies using a Spring Initializr. The screenshot below shows how to create an Order microservice by selecting the Spring Boot version, project type Maven, Java 11, and necessary dependencies:

alt_text

Creating an Order microservice via Spring Initializr

A Customer microservice is generated in a similar fashion.

Configure the Order microservice

Domain/Entity

The source code is organized in different packages as per the convention in the Domain-Driven Design. The Domain classes are the POJO classes used to shape the API data model. These POJOs are also helpful in persisting the data in MongoDB: its advantage is that we do not need any DTO layer for mapping the REST entities into the database.

Below you will find the POJO definitions I have for the Order microservice:

@Document(collection = "order")
@Getter
@Setter
@ToString
@NoArgsConstructor
public class Order implements Serializable {

   private static final long serialVersionUID = 1L;

   @Id
   private String id;

   @NotBlank
   @Field("customer_id")
   private String customerId;

   @Field("created_at")
   @CreatedDate
   private Instant createdAt;

   @Field("updated_at")
   @LastModifiedDate
   private Instant updatedAt;

   @Version
   public Integer version;

   @Field("status")
   private OrderStatus status = OrderStatus.CREATED;

   @Field("payment_status")
   private Boolean paymentStatus = Boolean.FALSE;

   @NotNull
   @Field("payment_method")
   private PaymentType paymentMethod;

   @NotNull
   @Field("payment_details")
   private String paymentDetails;

   @Field("shipping_address")
   private Address shippingAddress;

   @Field("products")
   @NotEmpty
   private Set<@Valid Product> products;

I’m referring to Project Lombok to reduce the boilerplate code (e.g., Getter, Setter, toString, HashCode, Equals). Moreover, I have used Bean Validation according to the Swagger definitions.

Automatic auditing of the orders in the database is possible with the annotations: @CreatedDate, @LastModifiedDate, and @Version.

The Order microservice has only one collection (Order), and all its other entities (e.g., products, shippingAddress) can be embedded in the Order collection.

Repository

I chose MongoDB as a database for my project. There are mainly two ways to access data in MongoDB. One is to use the Java Driver for MongoDB, which supports the native, low-level MongoDB API. Another, simpler option is Spring Data MongoDB. Spring Data is an initiative from Spring to unify and access different kinds of stores (SQL, NoSQL, and others). It applies the repository concept from the Domain-Driven Design. In this demo, I am sticking to Spring Data MongoDB for ease of development.

I created the OrderRepository class (interface) to handle the Order entity persistence:

@Repository
public interface OrderRepository extends MongoRepository<Order, String> {
}

Controllers

Our microservice architecture exposes API so that a client or other microservices could communicate with it using REST. Spring MVC offers a controller pattern to expose REST API with minimum effort.

For any application, the best practice is to add a /health endpoint to check whether the application is running. It is also present in Kubernetes to check the liveness of the application by sending “heartbeat” requests.

First, create the Health class:

@Data
@NoArgsConstructor
@EqualsAndHashCode
@ToString
public class Health {
   private HealthStatus status;
}

And then — HealthStatus class.

public enum HealthStatus {
   UP("UP"),
   DOWN("DOWN");

   private final String status;

   HealthStatus(String status) {
       this.status = status;
   }

   public String getStatus() {
       return status;
   }

   @Override
   public String toString() {
       return status;
   }
}

Here’s the controller for the /health endpoint:

@RestController
@RequestMapping("/api/v1")
public class HealthResource {
   private final Logger log = LoggerFactory.getLogger(HealthResource.class);

   @GetMapping(
           value = "/health",
           produces = "application/json")
   public ResponseEntity<Health> getHealth() {
       log.debug("REST request to get the Health Status");
       final var health = new Health();
       health.setStatus(HealthStatus.UP);
       return ResponseEntity.ok().body(health);
   }
}

For the Order microservice, the service endpoints are defined in the OrderResource class, offering the REST API for the order-related CRUD operations. For the sake of brevity, we will now create only one method, createOrder (POST request).

@RestController
@RequestMapping("/api/v1")
public class OrderResource {
   private final Logger log = LoggerFactory.getLogger(OrderResource.class);

   private static final String ENTITY_NAME = "order";

   @Value("${spring.application.name}")
   private String applicationName;

   private final OrderRepository orderRepository;

   private final OrderService orderService;

   public OrderResource(OrderRepository orderRepository, OrderService orderService) {
       this.orderRepository = orderRepository;
       this.orderService = orderService;
   }

   @PostMapping("/orders")
   @Transactional
   public ResponseEntity<Order> createOrder(@Valid @RequestBody Order order) throws URISyntaxException {
       log.debug("REST request to save Order : {}", order);
       if (order.getId() != null) {
           throw new ResponseStatusException(HttpStatus.CONFLICT, "A new order cannot already have an ID");
       }
       final var result = orderRepository.save(order);
       orderService.createOrder(result);

       HttpHeaders headers = new HttpHeaders();
       String message = String.format("A new %s is created with identifier %s", ENTITY_NAME, result.getId().toString());
       headers.add("X-" + applicationName + "-alert", message);
       headers.add("X-" + applicationName + "-params", result.getId().toString());
       return ResponseEntity.created(new URI("/api/orders/" + result.getId())).headers(headers).body(result);
   }

}

The final touch is to add configuration files (application.yml, index.html) and an ApplicationConfiguration class.

The ApplicationConfiguration class is quite simple:

@Configuration
public class ApplicationConfiguration {
   @Bean
   public RestTemplate restTemplate(RestTemplateBuilder builder) {
       return builder.build();
   }
}

index.html file in the resources/static directory:

<!DOCTYPE html>
<html lang="en">
  <head>
     <meta charset="UTF-8">
     <title>Order Microservice</title>
  </head>
  <body>
    <h1>Welcome to the Order Microservice</h1>
    <p> Current Time:
       <script> document.write(new Date().toLocaleTimeString()); </script>
    </p>
    <p>The order endpoints are <a href="/order/api/v1/orders">here</a></p>
  </body>
</html>

And finally, two application.yml files, one in the resources directory:

spring:
 application:
   name: microservice-order

   microservice-customer:
     url: https://customer.microservicesdemo.net/customer/api/v1/

 data:
   mongodb:
     uri: mongodb+srv://mkmongouser:[email protected]
     database: order

server:
 port: 8080
 servlet:
   context-path: /order

and one in the local directory:

spring:
 application:
   name: microservice-order
   microservice-customer:
     url: http://localhost:8080/customer/api/v1/

 devtools:
   restart:
     enabled: true

 data:
   mongodb:
     uri: mongodb://localhost:27017
     database: order

server:
 port: 8080
 servlet:
   context-path: /order

Configure the Customer microservice

As for the Customer microservice, it also contains ApplicationConfiguration, Health, HealthStatus, and HealthResource classes identical to those in the Order microservice, so just copy and paste them into your Customer application.

Now, let’s define the POJOs for a Customer class.

@Document(collection = "customer")
@Data
@NoArgsConstructor
public class Customer implements Serializable {

   private static final long serialVersionUID = 1L;

   @Id
   private String id;
   @Field("orders")
   private Set<Order> orders = new HashSet<>();

   public Customer addOrder(Order order) {
       this.orders.add(order);
       return this;
   }
}
@Getter
@Setter
@ToString
@NoArgsConstructor
public class Order implements Serializable {

   private static final long serialVersionUID = 1L;

   @Id
   @NotBlank
   private String id;

   @NotBlank
   private String customerId;

   @Override
   public boolean equals(Object o) {
       if (this == o) return true;
       if (o == null || getClass() != o.getClass()) return false;
       Order order = (Order) o;
       return id.equals(order.id);
   }

   @Override
   public int hashCode() {
       return Objects.hash(id);
   }
}

Create a CustomerRepository interface similar to the OrderRepository:

@Repository
public interface CustomerRepository extends MongoRepository<Customer, String> {
}

The service endpoints are defined in the CustomerResource class, offering the REST API for the customer-related CRUD operations.

@RestController
@RequestMapping("/api/v1")
public class CustomerResource {

   private final Logger log = LoggerFactory.getLogger(CustomerResource.class);

   private static final String ENTITY_NAME = "customer";

   @Value("${spring.application.name}")
   private String applicationName;

   private final CustomerRepository customerRepository;

   public CustomerResource(CustomerRepository customerRepository) {
       this.customerRepository = customerRepository;
   }

   @PostMapping("/customers")
   public ResponseEntity<Customer> createCustomer(@Valid @RequestBody Customer customer) throws URISyntaxException {
       log.debug("REST request to save Customer : {}", customer);
       if (customer.getId() != null) {
           throw new ResponseStatusException(HttpStatus.CONFLICT, "A new customer cannot already have an ID");
       }
       final var result = customerRepository.save(customer);

       HttpHeaders headers = new HttpHeaders();
       String message = String.format("A new %s is created with identifier %s", ENTITY_NAME, customer.getId());
       headers.add("X-" + applicationName + "-alert", message);
       headers.add("X-" + applicationName + "-params", customer.getId());
       return ResponseEntity.created(new URI("/api/customers/" + result.getId())).headers(headers).body(result);
   }
}

Here we also have the CustomerOrderResource class offering the REST API to establish a link between the Customer and the Order.

@RestController
@RequestMapping("/api/v1")
public class CustomerOrderResource {

   private final Logger log = LoggerFactory.getLogger(CustomerOrderResource.class);

   private static final String ENTITY_NAME = "order";

   @Value("${spring.application.name}")
   private String applicationName;

   private final CustomerRepository customerRepository;

   public CustomerOrderResource(CustomerRepository customerRepository) {
       this.customerRepository = customerRepository;
   }

   @PostMapping("/customerOrders/{customerId}")
   public ResponseEntity<Order> createOrder(@PathVariable String customerId, @Valid @RequestBody Order order) {
       log.debug("REST request to save Order : {} for Customer ID: {}", order, customerId);
       if (customerId.isBlank()) {
           throw new ResponseStatusException(
                   HttpStatus.NOT_FOUND, "No Customer: " + ENTITY_NAME);
       }
       final Optional<Customer> customerOptional = customerRepository.findById(customerId);
       if (customerOptional.isPresent()) {
           final var customer = customerOptional.get();
           customer.addOrder(order);
           customerRepository.save(customer);
           return ResponseEntity.ok()
                   .body(order);
       } else {
           throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid Customer: " + ENTITY_NAME);
       }
   }
}

Finally, define the configuration files, index.html and two application.yml files.

index.html file:

<!DOCTYPE html>
<html lang="en">
  <head>
     <meta charset="UTF-8">
     <title>Customer Microservice</title>
  </head>
  <body>
     <h1>Welcome to the Customer Microservice</h1>
     <p> Current Time:
         <script> document.write(new Date().toLocaleTimeString()); </script>
     </p>
     <p>The customer endpoints are <a href="/customer/api/v1/customers">here</a></p>
     <p>The customer-order endpoints are <a href="/customer/api/v1/customerOrders">here</a></p>
  </body>
</html>

application.yml file in the main/local directory:

spring:
 application:
   name: microservice-customer

 devtools:
   restart:
     enabled: true
 data:
   mongodb:
     uri: mongodb://localhost:27017
     database: customer

server:
 port: 8080
 servlet:
   context-path: /customer

application.yml file in the resources directory:

spring:
 application:
   name: microservice-customer

 data:
   mongodb:
     uri: mongodb+srv://mkmongouser:[email protected]
     database: customer

server:
 port: 8080
 servlet:
   context-path: /customer

Services

In Spring projects, the business logic is encapsulated by a service layer. The Order microservice additionally synchronizes with the Customer one and this logic in the OrderService class.

@Service
@Slf4j
public class OrderService {

   private final Logger log = LoggerFactory.getLogger(OrderService.class);

   @Autowired
   RestTemplate restTemplate;

   @Autowired
   ObjectMapper objectMapper;

   @Value("${spring.application.microservice-customer.url}")
   private String customerBaseUrl;

   private static final String CUSTOMER_ORDER_URL = "customerOrders/";

   public void createOrder(Order order) {
       final var url = customerBaseUrl + CUSTOMER_ORDER_URL + order.getCustomerId();
       final var headers = new HttpHeaders();
       headers.setContentType(MediaType.APPLICATION_JSON);

       log.info("Order Request URL: {}", url);
       try {
           final var request = new HttpEntity<>(order, headers);
           final var responseEntity = restTemplate.postForEntity(url, request, Order.class);
           if (responseEntity.getStatusCode().isError()) {
               log.error("For Order ID: {}, error response: {} is received to create Order in Customer Microservice",
                       order.getId(),
                       responseEntity.getStatusCode().getReasonPhrase());
               throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, String.format("For Order UUID: %s, Customer Microservice Message: %s", order.getId(), responseEntity.getStatusCodeValue()));
           }
           if (responseEntity.hasBody()) {
               log.error("Order From Response: {}", responseEntity.getBody().getId());
           }
       } catch (Exception e) {
           log.error("For Order ID: {}, cannot create Order in Customer Microservice for reason: {}", order.getId(), ExceptionUtils.getRootCauseMessage(e));
           throw new ResponseStatusException(HttpStatus.BAD_REQUEST, String.format("For Order UUID: %s, Customer Microservice Response: %d", order.getId(), ExceptionUtils.getRootCauseMessage(e)));
       }
   }
}

Conclusion

In this first part of the series, we studied a demo backend Spring Boot application based on microservice architecture. We got the source code with the Controller, Service, Repository, and Entity model. Now the code is compiled and ready for deployment. Both microservices can be enhanced further, with custom exceptions classes, detailed data on customers and orders, etc. Feel free to populate your microservices on your own or head over to my GitHub repository, where I have full-blown Customer and Order microservices with all the additional data.

Part two will look into how to prepare and run these microservices in the cloud.

Announcements
Author image

Md Kamaruzzaman

Software Architect, Special for BellSoft

 Twitter

 LinkedIn

 Blog