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.
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:
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.
Unlocking the bonus: A new Linux tailor-made for Spring apps
BellSoft created a lightweight Linux distribution for Cloud and Server use — Alpaquita Linux. Based on Alpine, it boasts small size and a bunch of distinctive features making it a perfect Linux for Java applications:
- Optimized musl, whose performance is equal or superior to that of glibc
- Additional glibc-based variant
- Three additional malloc implementations for various Java workloads
- Packages with Java tools
- Perfect compatibility with Liberica JDK Lite and Liberica NIK
- Support from BellSoft, a major OpenJDK contributor
Migration to Alpaquita Containers based on Linerica JDK Lite and Alpaquita is effortless and convenient but brings immediate advantages in terms of reduced memory consumption. Try Alpaquita Containers with your Spring Boot project and tell us what you think!