Is containerization the future of Java development? Discover the answer in 2021 Containers Trend Report: Download now!

Building Cloud-Native Java Microservices with OpenJDK

Building a Java Microservices E-Commerce App with Liberica JDK. Part 1: Preparation is Half the Battle!


Published May 07, 2021


BellSoft Blog Disclaimer

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 articles, either an introduction to understanding the architecture or the one on building a microservice.

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 Java application for an online store based on open source Liberica JDK. The three-part series is going to cover everything from designing to deploying to testing and monitoring. Part 1 will focus on the application’s structure, tools and source code—fully preparing for deployment on the cloud. Below you will even find samples that are copy and paste ready. Enjoy!

Are you currently working to create an online store? 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 e-commerce software with our senior engineer.

Case Study

Developing a full-blown e-commerce app will be too much for a blog post. So, for our case study, I’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.

In order to prepare for deployment, we need to make several important choices.

Domain-Driven Design

The first and foremost task is to find the core, supporting, and generic subdomains of the application. In DDD terms, it is called Strategic Design.

In practice, the IT and the domain experts (business) should work together to find the domains and the bounded context of the domains. If we think about the business model, then the bounded context is the model boundary inside which the same ubiquitous language is used.

Here is the strategic design of an e-commerce application with the domain modeling:

Strategic design

As we can see, ordering products is the core domain for an e-commerce application. Also, there are some supporting subdomains (Product, Customer, Cart, and Delivery) mandatory for an online store. Usually, the core domain and supporting subdomains are developed internally.

Some generic, cross-cutting services are also essential for an e-commerce store like Payment, Recommendation Engine, Authentication, and Authorization (RBAC, ACL). They are called the generic subdomain and usually outsourced as there are many COTS (commercial off-the-shelf) applications available for those generic purposes. Please note that large real-world online shops would include more supporting and generic subdomains.

For this demo, we will develop the core domain (Order) and one supporting subdomain (Customer).

Database per Microservice

When you are interested in a short-term gain (like boosting initial development velocity), a single database is a better choice. But for long-term benefits, what microservices is all about, I’ll use the database-per-microservice approach, where both the Product and Customer microservices will have separate databases. In this case study, we need an OLTP database supporting the ACID transnational guarantee; any SQL database or a handful of NoSQL ones may apply here. However, I will choose MongoDB, the most popular among NoSQLs and the most widely utilized document database. Since version 4.0, MongoDB also offers a cross-table ACID transactional guarantee with its WiredTiger storage engine. As described in detail here, managing transactions in microservices offers completely new sets of challenges. For our demo, we will accept the eventual consistency and won’t implement the distributed transaction.

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.

Cloud-Native Infrastructure over Libraries/Frameworks

In the microservice architecture, we have to deal with common issues like service discovery, load balancers, scaling, and fault tolerance. Netflix OSS has some excellent libraries that would help to address those issues. But our microservices will then be tightly coupled with a specific programming language.

In the future, if we decide to migrate them to other languages, we will face issues. In the future posts, I’ll turn to infrastructure, like Docker and Kubernetes, to address this.

This part of the series only shows the source code and structure without any deployment.

Synchronous Communication

I will start with synchronous communication using REST because it is simple to implement and a popular way of communication.

Component View

Here is the component view of the demo:

Component view

Here we are covering only the Customer and Order microservices. Microservice architecture follows the single responsibility principle and each service handles only one specific task.

In this demo, the Customer microservice will deal with every customer-related action: Create, Update, Delete, and Feed customers.It will do much more in a real e-commerce shop (like activate/deactivate customers, onboarding, etc.). Our Customer microservice will only do the basic things for the sake of simplicity.

Similarly, the Order microservice will manage everything regarding an order: Create, Read, Update and Delete orders. As I’ve already mentioned, they will have a separate data store (database/table/collection).

The frontend (web, desktop, and mobile) or the REST clients will connect with these microservices via REST API, i.e., REST API will serve as the contact between the frontend and the backend.

The Order microservice in our example needs to communicate with the Customer one. When a customer orders via the Order frontend, an Order is created in the Order database. Then it uses the “customerOrders” API to link the Order with the Customer. The Customer microservice will then maintain the link between the Customer and Order.

If you want to know more about how to code microservices the right way, you can read my post Microservice Architecture and its 10 Most Important Design Patterns on Medium.

Implementation

This use case will employ API-driven development, meaning we will first define the microservices API. It will then enable individual teams to work on the implementation without waiting for others. We’re going to apply the Swagger 2.0 REST API specification.

System requirements:

  • JDK 11;
  • Gradle;
  • MongoDB (Local or Atlas cluster);
  • Docker;
  • Kubernetes (kubectl CLI);
  • An AWS Account;
  • AWS CLI Version 2;
  • Eksctl CLI.

API Definition

Here’s the Swagger 2.0 API definition for the Order microservice endpoints:

alt_text

And the Swagger 2.0 API Model definitions:

alt_text

For the Customer microservice, we also have the Swagger API definitions:

alt_text

And its Swagger 2.0 data models:

alt_text

Basically, the Customer microservice has the same models as the Order one plus an additional model.

Database

I’ve chosen a widespread primary database in this project, MongoDB. Like most NoSQL databases, it is partition tolerant, so it offers automatic backup and horizontal scaling using Replica Set. Maintaining a MongoDB cluster, on the other hand, requires some effort.

Fortunately, MongoDB Inc. provides a multi-cloud MongoDB as a Service with MongoDB Atlas. For my demo, I will create a free version of the MongoDB cluster. While the free version is not suitable for production load, it is enough for our sample.

In creating a MongoDB Atlas cluster, the first order of business is to select a cloud provider and region. As I am making the demo for Amazon Web Services, I’ve picked AWS and “Frankfurt” as shown below:

alt_text

Once you press the “Create Cluster” button, a cluster appears within 1–3 minutes. Now it’s time to create a user. In MongoDB Atlas, it is possible to create users with passwords, certificates or AWS IAM. Again, here I’ve taken the simplest approach and created one with a username and password:

alt_text

Once the user is created, we need to generate the connection string for the Atlas cluster so that our Spring Boot microservices can connect with it. Please note that the connection string is dependent on the programming language and your MongoDB version. Here’s an example of creating a connection string for Java applications:

alt_text

Projects

Creating a Spring Boot application with all the dependencies is challenging. Fortunately, Spring Boot offers a way to create a project with Spring Initializr. This screenshot shows how to make the Customer microservice using Spring Initializr, Java 11, and Gradle:

alt_text

The Order microservice Spring Boot scaffolder project is created in a similar fashion.

Source Code

The source code is again organized in different packages as per the convention in the Domain-Driven Design.

Domain/Entity

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 in the case of 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 will have only one collection (Order), and all its other entities (e.g., products, shippingAddress) will be embedded in the Order collection.

@Data
@NoArgsConstructor
public class Address implements Serializable {

   private static final long serialVersionUID = 2L;

   @Id
   private String id;

   @NotNull
   private String streetName;

   @NotNull
   private String streetNumber;

   private String additionalInfo;

   @NotNull
   private String zipCode;

   @NotNull
   private String city;

   private String state;

   @NotNull
   private String country;
@Data
@NoArgsConstructor
public class Product implements Serializable {

   private static final long serialVersionUID = 2L;

   @Id
   @NotNull
   private String id;

   @NotNull
   private String name;

   private String description;

   private String modelNumber;

   @NotNull
   private String manufacturerName;

   @NotNull
   @Min(value = 0, message = "Price cannot be less than zero")
   private Double price;

   private String detailInfo;

   private String imageUrl;

   @NotNull
   @Min(value = 0, message = "Quantity cannot be less than zero")
   private Integer quantity;

Repository

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. Here in this demo, I am sticking to Spring Data MongoDB for ease of development.

Below is the OrderRepository class to handle the Order entity persistence:

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

}

@Repository
public interface CustomerRepository extends MongoRepository<Customer, 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.

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.

@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;
   }

   /**
    * {@code POST  /orders} : Create a new order.
    *
    * @param order the order to create.
    * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new order, or with status {@code 400 (Bad Request)} if the order has already an ID.
    * @throws URISyntaxException if the Location URI syntax is incorrect.
    */
   @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 BadRequestAlertException("A new order cannot already have an ID", ENTITY_NAME, "idexists");
       }
       final var result = orderRepository.save(order);
       orderService.createOrder(result);
       return ResponseEntity.created(new URI("/api/orders/" + result.getId()))
           .headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, result.getId().toString()))
           .body(result);
   }

   /**
    * {@code PUT  /orders} : Updates an existing order.
    *
    * @param order the order to update.
    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated order,
    * or with status {@code 400 (Bad Request)} if the order is not valid,
    * or with status {@code 500 (Internal Server Error)} if the order couldn't be updated.
    * @throws URISyntaxException if the Location URI syntax is incorrect.
    */
   @PutMapping("/orders")
   @Transactional
   public ResponseEntity<Order> updateOrder(@Valid @RequestBody Order order) throws URISyntaxException {
       log.debug("REST request to update Order : {}", order);
       if (order.getId() == null) {
           throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull");
       }
       final var result = orderRepository.save(order);
       orderService.updateOrder(result);
       return ResponseEntity.ok()
           .headers(HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, order.getId().toString()))
           .body(result);
   }

   /**
    * {@code GET  /orders} : get all the orders.
    *

    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of orders in body.
    */
   @GetMapping("/orders")
   @Transactional
   public List<Order> getAllOrders() {
       log.debug("REST request to get all Orders");
       return orderRepository.findAll();
   }

   /**
    * {@code GET  /orders/:id} : get the "id" order.
    *
    * @param id the id of the order to retrieve.
    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the order, or with status {@code 404 (Not Found)}.
    */
   @GetMapping("/orders/{id}")
   @Transactional
   public ResponseEntity<Order> getOrder(@PathVariable String id) {
       log.debug("REST request to get Order : {}", id);
       final var order = orderRepository.findById(id);
       return ResponseUtil.wrapOrNotFound(order);
   }

   /**
    * {@code DELETE  /orders/:id} : delete the "id" order.
    *
    * @param id the id of the order to delete.
    * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}.
    */
   @DeleteMapping("/orders/{id}")
   @Transactional
   public ResponseEntity<Void> deleteOrder(@PathVariable String id) {
       log.debug("REST request to delete Order : {}", id);
       final var orderOptional = orderRepository.findById(id);
       orderRepository.deleteById(id);
       if (orderOptional.isPresent()) {
         orderService.deleteOrder(orderOptional.get());
       };
       return ResponseEntity.noContent().headers(HeaderUtil.createEntityDeletionAlert(applicationName, false, ENTITY_NAME, id)).build();
   }
}

Note that I’m using the relevant classes from jhipster for exception (e.g., BadRequestAlertException) and response handling (e.g., HeaderUtil, ResponseUtil).

As for the Customer microservice, 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;
   }

   /**
    * {@code POST  /customers} : Create a new customer.
    *
    * @param customer the customer to create.
    * @return the {@link ResponseEntity} with status {@code 201 (Created)} and with body the new customer, or with status {@code 400 (Bad Request)} if the customer has already an ID.
    * @throws URISyntaxException if the Location URI syntax is incorrect.
    */
   @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 BadRequestAlertException("A new customer cannot already have an ID", ENTITY_NAME, "idexists");
       }
       final var result = customerRepository.save(customer);
       return ResponseEntity.created(new URI("/api/customers/" + result.getId()))
           .headers(HeaderUtil.createEntityCreationAlert(applicationName, false, ENTITY_NAME, result.getId()))
           .body(result);
   }

   /**
    * {@code PUT  /customers} : Updates an existing customer.
    *
    * @param customer the customer to update.
    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated customer,
    * or with status {@code 400 (Bad Request)} if the customer is not valid,
    * or with status {@code 500 (Internal Server Error)} if the customer couldn't be updated.
    * @throws URISyntaxException if the Location URI syntax is incorrect.
    */
   @PutMapping("/customers")
   public ResponseEntity<Customer> updateCustomer(@Valid @RequestBody Customer customer) throws URISyntaxException {
       log.debug("REST request to update Customer : {}", customer);
       if (customer.getId() == null) {
           throw new BadRequestAlertException("Invalid id", ENTITY_NAME, "idnull");
       }
       final var result = customerRepository.save(customer);
       return ResponseEntity.ok()
           .headers(HeaderUtil.createEntityUpdateAlert(applicationName, false, ENTITY_NAME, customer.getId()))
           .body(result);
   }

   /**
    * {@code GET  /customers} : get all the customers.
    *

    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of customers in body.
    */
   @GetMapping("/customers")
   public List<Customer> getAllCustomers() {
       log.debug("REST request to get all Customers");
       return customerRepository.findAll();
   }

   /**
    * {@code GET  /customers/:id} : get the "id" customer.
    *
    * @param id the id of the customer to retrieve.
    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the customer, or with status {@code 404 (Not Found)}.
    */
   @GetMapping("/customers/{id}")
   public ResponseEntity<Customer> getCustomer(@PathVariable String id) {
       log.debug("REST request to get Customer : {}", id);
       final var customer = customerRepository.findById(id);
       return ResponseUtil.wrapOrNotFound(customer);
   }

   /**
    * {@code DELETE  /customers/:id} : delete the "id" customer.
    *
    * @param id the id of the customer to delete.
    * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}.
    */
   @DeleteMapping("/customers/{id}")
   public ResponseEntity<Void> deleteCustomer(@PathVariable String id) {
       log.debug("REST request to delete Customer : {}", id);
       customerRepository.deleteById(id);
       return ResponseEntity.noContent().headers(HeaderUtil.createEntityDeletionAlert(applicationName, false, ENTITY_NAME, id)).build();
   }
}

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;
   }

   /**
    * {@code POST  /orders/:customerId} : Create a new order for the given "customerId" customer.
    *
    * @param customerId the id of the customer.
    * @param order      the order to create.
    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the new order, or with status {@code 400 (Bad Request)} if the order has already an ID.
    * @throws URISyntaxException if the Location URI syntax is incorrect.
    */
   @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 (StringUtils.isBlank(customerId)) {
           throw new BadRequestAlertException("No Customer", ENTITY_NAME, "noid");
       }
       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 BadRequestAlertException("Invalid Customer", ENTITY_NAME, "invalidcustomer");
       }
   }

   /**
    * {@code PUT  /orders/:customerId} : Updates an existing order for the given "customerId" customer.
    *
    * @param customerId the id of the customer.
    * @param order      the order to update.
    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the updated order,
    * or with status {@code 400 (Bad Request)} if the order is not valid,
    * or with status {@code 500 (Internal Server Error)} if the order couldn't be updated.
    */
   @PutMapping("/customerOrders/{customerId}")
   public ResponseEntity<Order> updateOrder(@PathVariable String customerId, @Valid @RequestBody Order order) {
       if (StringUtils.isBlank(customerId)) {
           throw new BadRequestAlertException("No Customer", ENTITY_NAME, "noid");
       }
       final Optional<Customer> customerOptional = customerRepository.findById(customerId);
       if (customerOptional.isPresent()) {
           final var customer = customerOptional.get();
           final var orderSet = customer.getOrders().stream().map(o -> Objects.equals(o.getId(), order.getId()) ? order : o).collect(Collectors.toSet());
           customer.setOrders(orderSet);
           customerRepository.save(customer);
           return ResponseEntity.ok()
                   .body(order);
       } else {
           throw new BadRequestAlertException("Invalid Customer", ENTITY_NAME, "invalidcustomer");
       }
   }

   /**
    * {@code GET  /orders} : get all the orders.
    *
    * @param customerId the id of the customer.
    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of orders in body.
    */
   @GetMapping("/customerOrders/{customerId}")
   public Set<Order> getAllOrders(@PathVariable String customerId) {
       log.debug("REST request to get all Orders for Customer: {}", customerId);
       if (StringUtils.isBlank(customerId)) {
           throw new BadRequestAlertException("No Customer", ENTITY_NAME, "noid");
       }
       final var customerOptional = customerRepository.findById(customerId);
       if (customerOptional.isPresent()) {
           final var customer = customerOptional.get();
           return customer.getOrders();
       } else {
           throw new BadRequestAlertException("Invalid Customer", ENTITY_NAME, "invalidcustomer");
       }
   }

   /**
    * {@code GET  /orders/:customerId/:orderId} : get the "orderId" order for the "customerId" customer.
    *
    * @param customerId the id of the customer.
    * @param orderId    the id of the order to retrieve.
    * @return the {@link ResponseEntity} with status {@code 200 (OK)} and with body the order, or with status {@code 404 (Not Found)}.
    */
   @GetMapping("/customerOrders/{customerId}/{orderId}")
   public ResponseEntity<Order> getOrder(@PathVariable String customerId, @PathVariable String orderId) {

       log.debug("REST request to get Order : {} for Customer: {}", orderId, customerId);
       if (StringUtils.isBlank(customerId)) {
           throw new BadRequestAlertException("No Customer", ENTITY_NAME, "noid");
       }
       final var customerOptional = customerRepository.findById(customerId);
       if (customerOptional.isPresent()) {
           final var customer = customerOptional.get();
           final var orderOptional = customer.getOrders().stream().filter(order -> Objects.equals(order.getId(), orderId)).findFirst();
           return ResponseUtil.wrapOrNotFound(orderOptional);
       } else {
           throw new BadRequestAlertException("Invalid Customer", ENTITY_NAME, "invalidcustomer");
       }
   }

   /**
    * {@code DELETE  /orders/:customerId/:orderId} : delete the "orderId" order for the "customerId" customer.
    *
    * @param customerId the id of the customer.
    * @param orderId    the id of the order to delete.
    * @return the {@link ResponseEntity} with status {@code 204 (NO_CONTENT)}.
    */
   @DeleteMapping("/customerOrders/{customerId}/{orderId}")
   public ResponseEntity<Void> deleteOrder(@PathVariable String customerId, @PathVariable String orderId) {
       log.debug("REST request to delete Order : {} for Customer: {}", orderId, customerId);
       if (StringUtils.isBlank(customerId)) {
           throw new BadRequestAlertException("No Customer", ENTITY_NAME, "noid");
       }
       final Optional<Customer> customerOptional = customerRepository.findById(customerId);
       if (customerOptional.isPresent()) {
           final var customer = customerOptional.get();
           customer.getOrders().removeIf((order) -> Objects.equals(order.getId(), orderId));
           customerRepository.save(customer);
           return ResponseEntity.noContent().build();
       } else {
           throw new BadRequestAlertException("Invalid Customer", ENTITY_NAME, "invalidcustomer");
       }
   }
}

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 {

   @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 CustomerOrderException(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 CustomerOrderException(order.getId(), ExceptionUtils.getRootCauseMessage(e));
       }
   }

   public void updateOrder(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);
           restTemplate.put(url, request);
       } catch (Exception e) {
           log.error("For Order ID: {}, cannot create Order in Customer Microservice for reason: {}", order.getId(), ExceptionUtils.getRootCauseMessage(e));
           throw new CustomerOrderException(order.getId(), ExceptionUtils.getRootCauseMessage(e));
       }
   }

   public void deleteOrder(Order order) {
       final var url = customerBaseUrl + CUSTOMER_ORDER_URL + order.getCustomerId() + "/" + order.getId();

       log.info("Order Request URL: {}", url);
       try {
           restTemplate.delete(url);
       } catch (Exception e) {
           log.error("For Order ID: {}, cannot create Order in Customer Microservice for reason: {}", order.getId(), ExceptionUtils.getRootCauseMessage(e));
           throw new CustomerOrderException(order.getId(), ExceptionUtils.getRootCauseMessage(e));
       }
   }
}

Conclusion

In this first part of the series, we started developing a demo backend e-commerce application based on microservice architecture. I have described its DDD-inspired design, defined the tech stack (Liberica JDK + Spring Boot), picked MongoDB as our database and AWS as the cloud service provider. Then we got the source code with Controller, Service, Repository, and Entity model. Now the code is compiled and ready for deploying.

Part 2 will look into how to prepare and run these microservices in the cloud. Don’t want to miss it? Subscribe to our newsletter below, and we’ll send the post right to you when it comes out!

Author image

Md Kamaruzzaman

Software Architect, Special for BellSoft

 Twitter

 LinkedIn

 Blog

BellSoft LTD [email protected] BellSoft LTD logo Liberica Committed to Freedom 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 BellSoft LTD 199 Obvodnogo Kanala Emb. 190020 St. Petersburg RU +7 812-336-35-67 BellSoft LTD 111 North Market Street, Suite 300 CA 95113 San Jose US +1 702 213-59-59