
Quarkus. From Monolithic to a Microservice Architecture. How to Decompose using ‘Branch By Abstraction’ Pattern. Part 1.
‘Branch By Abstraction’ & ‘Database-Per-Service’ patterns implementation using Quarkus REST Client Reactive and Feature Toggles.E-commerce Case Study.
In the previews articles we started building the back-end system for our fictional e-shop. We have implemented 2 Resource Endpoints, the ProductResource and the OrderResource providing functionality for products and orders. However our system is sharing the same database with all the scalability problems arising from that.
It is time to break down our monolithic application into a micro-service architecture following the Database-Per-Service pattern and Branch By Abstraction Pattern. You can read more about this patterns by visiting microservices.io and also Martin Fowler’s blog.
Lets go into some theory first about database sharing between domains in a monolithic application and what are the options of communicating and sharing data between micro-services
A. Database-Per-Service Pattern
Breaking down a monolithic application with a shared database into a microservices architecture with separate databases is a complex process that comes with various challenges and issues. While microservices offer benefits like scalability, flexibility, and faster development, they also introduce complexities that need to be carefully managed.
Our order system is the one shown. When a new item is requested to be added to the order then the item is fetched from the local database using Panache ORM [1] as shown in figure 1.

In this series of articles, our goal is to implement a functionality where, upon the addition of a new item to an order, we retrieve both product data and stock availability information from a newly developed inventory microservice. To achieve this, we’ll utilize the Quarkus REST Client. The architecture we aim to build aligns with the representation in Figure 2.

B. InterService Communication using REST Client Reactive.
For this inter-service REST comunication for finding a unique product and validating the stock we are going to explore the Quarkus REST Client Reactive, an implementation of MicroProfile Rest Client, which provides a more convenient and type-safe way to invoke RESTful APIs compared to traditional approaches.
‘MicroProfile Rest Client is a part of the Eclipse MicroProfile project, which aims to provide a set of specifications for building microservices-based Java applications. MicroProfile Rest Client is specifically focused on simplifying the consumption of RESTful web services within Java microservices applications. ‘
Here are some key features and benefits of MicroProfile Rest Client:
Declarative API: MicroProfile Rest Client allows you to define RESTful API interfaces using annotations and Java interfaces. This declarative approach means you can create a Java interface that represents the REST API, and the client implementation is generated automatically.
Type Safety: The generated client code is strongly typed, which means you can use Java interfaces and data objects to interact with the REST service. This reduces the chances of runtime errors and makes your code more maintainable.
However Sharing product data through a REST API is not the fastest thing to do comparing to a solution using reference tables in a share database.
C. Monolithic to Micro-Service ‘Data’ Challenge.
Designing a microservice architecture where each microservice corresponds to a single entity may seem like an intuitive approach, but it can lead to excessive inter-service communication.
By definition, microservices should be self-contained units capable of performing their business functions independently. However, most business processes involve multiple entities.
In monolithic applications, it’s common for modules to access required data from a different module through an SQL join to the other table. In our case OrderItem has a reference to Products table as well as to Customers table.
The Latency Problem And Micro-Services Communication
If we decide to adopt a microservices approach, we must be prepared for changes in how data relationships are managed. In our new architecture, orders and products reside in completely different databases.
We may tempted to replace the traditional database join to products [2] by API calls between microservices, introducing latency.
Although the purpose of the article demonstrates the ‘Branch by Abstraction Pattern’ using Quarkus synchronous communication by no means it is the recommended approach for production.The correct approaches are:
Asynchronous Communication:Instead of synchronous communication, consider using asynchronous messaging patterns. This can be achieved through message queues or publish-subscribe systems. The order-service can send a request to the inventory-service and continue processing other tasks while awaiting a response.
Caching:Another technique is to implement caching mechanisms in the order-service to store frequently requested stock information. This can help reduce the need for frequent requests to the inventory service, especially for data that doesn’t change frequently.
Retry and Circuit Breaker Pattern: If you tempted for a synchronous communication FOR START then YOU MUST implement retry mechanisms with backoff strategies for failed requests to the inventory service. In Quarkus it is easy to use the Circuit Breaker Pattern to temporarily halt requests if the inventory service is experiencing issues, preventing cascading failures.
Batch Processing: If the order-service requires multiple pieces of information from the inventory service, consider the ‘Request Batch’ Pattern by batching requests to reduce the overhead of making individual calls. This can be particularly effective since there are multiple items to be checked in a single order.
Data Integrity Problem
Now, let’s address the concern about data integrity. In a microservices architecture, each microservice typically has its own database or separate schema in the same database.
This makes establishing traditional foreign key relationships between the product table of both order-service and inventory-service impractical. As a result, enforcing data integrity at the database level becomes challenging.
The Data Consistency Challenge
The challenge arises from the independent nature of these micro-services where each one manages its own data store and also another problem is that business transactions often involve interactions with multiple microservices. The real problem however is:
There is not a shared transactional mechanism of any kind
Unlike a monolithic system where transactions are typically managed within a single database, microservices operate independently with their own databases and there is not a shared transaction mechanism.
Ensuring consistent and synchronized data across microservices becomes a complex challenge. Scenarios where updates to one service succeed while updates to another may fail, leading to inconsistencies, are common
Achieving traditional ACID properties (Atomicity, Consistency, Isolation, Durability) across distributed services is difficult, nearly impossible if you are building a mission-critical strong consistency application.
To address these problems microservice architecture often rely on patterns like the Saga Pattern, Eventual Consistency, and Compensating Transactions to address the Data Consistency Problem. These patterns introduce mechanisms for managing transactions across services, handling failures, and
EVENTUALLY converging towards a consistent state.
ENENTUALLY. That is.
As noted by a great reader which I’m thankfull, the original ‘The Data Consistency Problem’ paragraph introduced confusion due to the use of the term “splitting business transactions.” You don’t actually split the transaction. The original paragraph:
“Another challenge is maintaining data consistency in a microservices architecture, as business transactions are split across multiple microservices. This necessitates the use of patterns like the SAGA pattern, which involves orchestrating local transactions and creating compensating transactions to handle rollbacks effectively.”
UUID for Product Records
In a microservices architecture, using UUIDs (Universally Unique Identifiers) for primary keys in database tables is a common practice, and it can be particularly beneficial when dealing with distributed systems and services like the order service and the inventory service. Here are some reasons why using UUIDs for the product table might be a good idea:
- Uniqueness: UUIDs are designed to be globally unique. This means that in our distributed environment with multiple services and a database per service , we can be reasonably sure that the UUIDs used for products will not collide or conflict with each other.
- Decoupling: UUIDs allow each microservice to generate its own unique local primary key identifiers without needing to coordinate with other services. This promotes independence and reduces the need for centralized identity management.
- Data Integration: When multiple services need to interact or share data, using UUIDs can simplify data integration. Since UUIDs are globally unique, they can serve as a common identifier across services.
- Data Replication: In our case stock and price information about a product needs to be replicated across databases, using Kafka messaging or other techniques. UUIDs make it easier to merge or synchronize data without conflicts.
- Avoiding Exposing Internal IDs: Using UUIDs can help prevent the exposure of internal database IDs to external clients, which can be a security best practice.
D. Monolithic to Micro-Service ‘Refactoring Code’ Challenge.
This is another important part. There a few patterns to migrate from our monolithic application to micro-services such as the Strangler Fig pattern, Pattern of Parallel Runs the pattern of Branch By Abstraction and much more.
If you want to dive deep into the theory read this very nice article Patterns to migrate from Monolith to Microservices .
According to Martin Fowler
“Branch by Abstraction” is a technique for making a large-scale change to a software system in gradual way that allows you to release the system regularly while the change is still in-progress.
Gradual. This is the key word. Branch by Abstraction we allow us to do gradual refactor our application. It promotes a structured and incremental approach.
Essentially using an abstraction layer will allow multiple implementations to co-exist in the software system, ensuring that the system builds and runs correctly and when we don’t longer want the old functionality we will remove it.
Lastly we didn’t choose the Strangle Fig Pattern since we are going to do refactoring and changes at component level .
Strangle Fig Pattern is more suited at the monolith’s perimeter by interpet calls of the endpoints using let’s say a proxy, façade or a load balancer .
These are the six phases that are being followed in Branch by Abstraction:
- Identify the Change: First, we need to identify the specific change or feature we want to introduce or modify in your software system. This change can range from refactoring code to adding new functionality or even upgrading a critical component.
- Create an Abstraction Layer: Instead of directly making changes to the existing codebase, we create an abstraction layer or interface that represents the new functionality or change. This abstraction layer acts as an intermediary between the old and new code.
- Implement the Change Separately: We implement the desired change or feature within this abstraction layer or interface. This allows to work on the new code independently of the existing codebase, reducing the risk of conflicts and errors.
- Gradual Integration: Once the new functionality is fully developed and tested within the abstraction layer, we can gradually integrate it into the existing codebase. This integration can be done incrementally, piece by piece, or module by module, depending on the complexity of the change and the structure of our codebase.
- Deprecate the Old Code: As we integrate the new code, we can start deprecating or phasing out the old code that the abstraction layer replaces. This can involve removing or refactoring the old code to use the new abstraction.
Enough with the theory! We our now ready to start!
STEP 1. Initialization. Add the Rest Client library.
In this article in order to share product stock info between our micro-services we emply a synchronous communication techique using a REST Client invocation library. In Quarkus that librasy is named ‘rest-client-reactive-jackson’, or ‘’rest-reactive’ .
REST Client Reactive is the REST Client implementation compatible with RESTEasy Reactive. This is the library needed in order to create REST Clients and interact with REST API Services or in quarkus terminology our ‘RESTEasy Reactive’ Services. Read more about it in Quarkus Official Documentation.
‘RESTEasy Reactive’ and ‘REST Client Reactive’ terminology doesn’t mean that our REST Endpoints and Services are written in a ‘reactive’ non-blocking style.
Remember this:
SERVER PART — REST Easy Reactive: Providing JSON REST Endpoints
CLIENT PART — REST Client Reactive: Invoking JSON REST Endpoints compatible with RESTEasy Reactive.
We are adding the rest-client-reactive-jackson in our order-service micro-service. Open up a terminal within the order-service project type the following:
quarkus extension add 'rest-client-reactive-jackson'Branch By Abstraction:STEP 1. Identify The Change
We said that we are going to fetch specific for the needs of our microservice product info from a new hypothetical micro-service named inventory-service through RESΤ API calls and not from the local database.
Thus we need to abstract the findProductById functionality at line [1].
OrderResource.java
..............
@Path("add/item/{orderId}")
@POST
public Response addOrderItem(@PathParam("orderId") Long orderId, OrderItem orderItemForm) {
[1] Product product = Product.findProductById(orderItemForm.productId);
Order.addOrderItem(orderId, product, orderItemForm.quantity);
return Response.status(Response.Status.OK).build();
}
.................Branch By Abstraction:STEP 2. Create the Abstraction Layer.
We start by creating a new Interface named IInventoryService and we are adding 2 methods for single product data and product availability validation named findProductByUUID and checkStockAvailability respectively, assuming our new inventory-service provides that functionality. As we said we introduce UUIDs for the main product identification mechanism.
IInventoryService.java
package com.platform.ecommerce.order.service;
import com.platform.ecommerce.order.model.InventoryProductRecord;
import com.platform.ecommerce.order.model.InventoryStockResultRecord;
import com.platform.ecommerce.order.model.Product;
public interface IInventoryService {
InventoryProductRecord findProductByUUID(String UUID);
InventoryStockResultRecord checkStockAvailability(String UUID, Integer quantity);
}We also introduce Java Records which can be a convenient choice for representing immutable Data Transfer Objects (DTOs) in our distributed micro-service environment to ex-change data between our micro-services. In addition we reduce all the boilerplate code such as writing getters, setters etc.
InventoryStockResultRecord for the inventory Product Stock Result
and InventoryProductRecord for Products.
InventoryStockResultRecord.java
package com.platform.ecommerce.order.model;
public record InventoryStockResultRecord(String productUUID, Integer availableQuantity, Integer requestedQuantity, Boolean available, Integer missingQuantity) {
public InventoryStockResultRecord(Product product, Integer requestedQuantity) {
this(product.UUID,product.getStock(),requestedQuantity, requestedQuantity < product.getStock() ? true : false, requestedQuantity > product.getStock() ? requestedQuantity - product.getStock() : 0);
}
}InventoryProductRecord.java
package com.platform.ecommerce.order.model;
public record InventoryProductRecord(String UUID, String name) {
public InventoryProductRecord(Product product) {
this(product.UUID, product.name);
}
}Clean! Java Records are Cool!
Branch By Abstraction:STEP 3. Implement the Change Part 1. Introduce Product UUIDs and Refactor Model Entities.
Since we introduce UUID as a global identifier of products we need to make a lot of refactoring changes to our model entities. Our Product entity introduces a new method findProductByUUID and the UUID column
Product.java
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import org.hibernate.annotations.CreationTimestamp;
import java.time.ZonedDateTime;
@Entity
public class Product extends PanacheEntity {
@Column(nullable = false,unique = true)
public String UUID;
@Column(nullable = false)
public String name;
@Column
private String description;
@Column
public Double price;
@Column(nullable = false)
public Integer stock;
//default stock value
{stock = 100;}
@CreationTimestamp
public ZonedDateTime created;
public Product() {}
public static Product findProductByUUID(String UUID) {
return find("UUID", UUID).firstResult();
}
public static Product findProductById(Long id) {
return findById(id);
}
public static Product findByName(String name){ return find("name", name).firstResult(); }
public Integer getStock() {
return stock;
}
}
We change the method signatures of addOrderItem, updateOrderItemQuantity and deleteOrderItem to use product UUID instead of the local database product primary key.
Order.java
package com.platform.ecommerce.order.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.logging.Log;
import io.quarkus.panache.common.Sort;
import jakarta.persistence.*;
import jakarta.transaction.Transactional;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.Column;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "orders")
public class Order extends PanacheEntity {
@Column(nullable = false)
public double totalAmount;
@Column(nullable = false)
public String status;
@JsonIgnore
@ManyToOne()
@JoinColumn(name = "customer_id", nullable = true)
public Customer customer;
@CreationTimestamp
@Column(updatable = false, nullable = false)
public ZonedDateTime created;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
public List<OrderItem> orderItems = new ArrayList<>();
@Transient
public void setTotalPrice() {
double total = 0D;
for (OrderItem item : orderItems) {
total += item.totalAmount;
}
totalAmount = total;
}
public static Order findOrderById(Long id) {
return findById(id);
}
public static List<Order> findOrdersByStatus(String status) {
List<Order> orderList = Order.list("status", Sort.by("customer.id").and("created"), status);
return orderList;
}
public static List<Order> findOrdersByCustomerId(Long customerId) {
List<Order> orderList = Order.list("customer.id", Sort.by("created"), customerId);
return orderList;
}
@Transactional
public static void createOrder(Long customerId, Order order) {
Customer customer = findById(customerId);
order.customer = customer;
order.persist();
}
@Transactional
public static void addOrderItem(Long orderId,String productUUID, Integer quantity) {
Order order = findOrderById(orderId);
Product product = Product.findProductByUUID(productUUID);
OrderItem orderItem = new OrderItem(order,product,quantity);
order.orderItems.add(orderItem);
order.setTotalPrice();
order.persist();
//orderItem.persist(); //CASCADING TYPE IS SET TO ALL SO NO NEEDED
}
@Transactional
public static void updateOrderItemQuantity(Long orderId, String productUUID, Integer quantity) {
Order order = findOrderById(orderId);
Log.info("Order found:"+order.id);
OrderItem orderItem = order.orderItems.stream().filter(o -> productUUID.equals(o.product.UUID)).findFirst().orElse(null);
Log.info("Order item found:"+orderItem.id);
orderItem.quantity = quantity;
Log.info("Update quantity to:"+quantity);
orderItem.setTotalPrice();
order.setTotalPrice();
order.persist();
}
@Transactional
public static void updateOrderStatus(Long id, String status) {
Order order = findOrderById(id);
order.status = status;
order.persist();
}
@Transactional
public static void deleteOrderItem(Long orderId, String productUUID) {
Order order = findOrderById(orderId);
Log.info("Order found:"+order.id);
OrderItem orderItem = order.orderItems.stream().filter(o -> productUUID.equals(o.productUUID)).findFirst().orElse(null);
Log.info("Order item found:"+orderItem.id);
order.orderItems.remove(orderItem);
Log.info("Remove item:"+orderItem.id);
order.setTotalPrice();
order.persist();
}
@Transactional
public static void deleteOrder(Long id) {
findById(id).delete();
}
}and our OrderItem entity becomes
OrderItem.java
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
@Entity
public class OrderItem extends PanacheEntity {
//default constructor
public OrderItem() {
}
public OrderItem(Order order, Product product, Integer quantity) {
this.order = order;
this.product = product;
this.productUUID = product.UUID;
this.quantity = quantity;
totalAmount = product.price*quantity;
}
public Long getId() {
return id;
}
@Column(nullable = false)
public double totalAmount;
@Column(nullable = false)
public Integer quantity;
@Transient
public String productUUID;
@OneToOne
@JoinColumn(name = "product_uuid",referencedColumnName = "uuid",insertable = true,updatable = false)
public Product product = new Product();
@ManyToOne(optional = false, fetch = FetchType.EAGER)
@JoinColumn(name="order_id", nullable=false)
@JsonIgnore
public Order order;
@Transient
public void setTotalPrice() {
totalAmount = product.price*quantity;
}
}
Branch By Abstraction:STEP 3. Implement the Change Part 2. Create the Service Layers.
We create 2 implementations of the IInventoryService interface. One for the already exisiting functionality for product ferching using a local database access and one for remote micro-service REST invocation of the inventory-service.
Our local implementation of the IInventoryService is ProductLocalDatabaseService which uses Panache ORM to fetch product data from our local database. It’s the default functionality of our monolith since we are not want to break the existing production system (also we are using the @DefaultBean annotation) but this time is using product UUID instead of the local database primary key of products table to fetch product records.
ProductLocalDatabaseService.java
package com.platform.ecommerce.order.service;
import com.platform.ecommerce.order.model.InventoryProductRecord;
import com.platform.ecommerce.order.model.InventoryStockResultRecord;
import com.platform.ecommerce.order.model.Product;
@ApplicationScoped
@DefaultBean
public class ProductLocalDatabaseService implements IInventoryService {
@Override
public InventoryProductRecord findProductByUUID(String UUID) {
Product product = Product.findProductByUUID(UUID);
InventoryProductRecord productResult = new InventoryProductRecord(product);
return productResult;
}
@Override
public InventoryStockResultRecord checkStockAvailability(String UUID, Integer requestedQuantity) {
Product product = Product.findProductByUUID(UUID);
InventoryStockResultRecord availabilityResult = new InventoryStockResultRecord(product,requestedQuantity);
return availabilityResult;
}
}Then we create a new implementation of our IInventoryService named ProductRemoteMicroService which supports REST API client invocation to our new inventory micro-service. This is essentially our ‘Branch’
Don’t worry about the @RestClient IProductInventoryClient at the moment. In the next step I explain in depth about it.
ProductRemoteMicroService.java
package com.platform.ecommerce.order.service;
import com.platform.ecommerce.order.model.InventoryProductRecord;
import com.platform.ecommerce.order.model.InventoryStockResultRecord;
import com.platform.ecommerce.order.rest.IProductInventoryClient;
import io.quarkus.arc.profile.UnlessBuildProfile;
import io.quarkus.arc.properties.UnlessBuildProperty;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@ApplicationScoped
@UnlessBuildProperty(name = "microservices.inventory.release.enabled", stringValue = "false")
//@UnlessBuildProfile("prod")
public class ProductRemoteMicroService implements IInventoryService {
IProductInventoryClient inventoryClient;
public ProductRemoteMicroService(@RestClient IProductInventoryClient inventoryClient) {
this.inventoryClient = inventoryClient;
}
@Override
public InventoryProductRecord findProductByUUID(String UUID) {
return inventoryClient.findProductByUUID(UUID);
}
@Override
public InventoryStockResultRecord checkStockAvailability(String UUID, Integer quantity) {
return inventoryClient.checkStockAvailability(UUID, quantity);
}
}Branch By Abstraction:STEP 4. Using feature toggles to switch between implementations
Sam Newman at his book “Monolith to Microservices” at page 108 introduces the idea of using ‘feature toggles’ to switch between the new and the old implementation when implementing branch by abstraction.
Therefore within application.properties of our order-service we also enter the following config value.
microservices.inventory.release.enabled = falseBut wait! What is the @UnlessBuildProperty(name = “microservices.inventory.release.enabled”, stringValue = “false”) annotation?
We want to conditionallly enable the new functionality based on quarkus build profiles such as prod, dev and test, or based on some sort of configuration entry in application properties. We are trying to break our monolith but not break the production system!
You can read more about it at the official quarkus documentation about contexts and dependency injection at 4.7. Enabling Beans for Quarkus Build Profile.
Tip: To conditionally enable our new implementation based on the build profiles use the @io.quarkus.arc.profile.IfBuildProfile and @io.quarkus.arc.profile.UnlessBuildProfile annotations. For example we want our ProductRemoteMicroService to run only at test and dev profiles and not in production. We annotate like this to protect our production system and have the new functionality ONLY WHEN TESTING
@UnlessBuildProfile("prod")
OR
@IfBuildProfile("test")We then insert this new inventory data service layer as well as the product availability validation code and product fetching code in our OrderResource EndPoint at [1], [2].
OrderResource.java
package com.platform.ecommerce.order.resource;
import com.platform.ecommerce.order.model.InventoryProductRecord;
import com.platform.ecommerce.order.model.InventoryStockResultRecord;
import com.platform.ecommerce.order.model.*;
import com.platform.ecommerce.order.service.IInventoryService;
import io.quarkus.logging.Log;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
@Path("order-service/v1/order")
@ApplicationScoped
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class OrderResource {
@Inject
IInventoryService inventoryService;
@GET
@Path("all")
public List<Order> getAllOrders() {return Order.listAll(); }
@GET
@Path("{id}")
public Response findById(@PathParam("id") Long id) {
return Order.findByIdOptional(id)
.map(order -> Response.ok(order).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
@POST
@Path("create/{customerId}")
public Response createOrder(@PathParam("customerId") Long customerId, Order order) {
Order.createOrder(customerId,order);
Log.info("Order created:"+order.id);
return Response.status(Response.Status.CREATED).build();
}
@PUT
@Path("update/status/{id}")
public Response updateOrderStatus(@PathParam("id") Long id, String status) {
Order.updateOrderStatus(id,status);
return Response.status(Response.Status.NO_CONTENT).build();
}
@Path("add/item/{orderId}")
@POST
public Response addOrderItem(@PathParam("orderId") Long orderId, OrderItem orderItemForm) {
[1] InventoryStockResultRecord inventoryStockResult = inventoryService.checkStockAvailability(orderItemForm.productUUID, orderItemForm.quantity);
Log.infof(":requested stock validation check from inventory for item: %s. Available quantity is: %s", inventoryStockResult.productUUID(),inventoryStockResult.availableQuantity());
if(inventoryStockResult.available()) {
Log.info("inventory stock validation passed");
[2] InventoryProductRecord inventoryProductResult = inventoryService.findProductByUUID(orderItemForm.productUUID);
Log.infof(":requested details for item with id: %s from inventory",inventoryProductResult.UUID());
Order.addOrderItem(orderId,orderItemForm.productUUID, orderItemForm.quantity);
return Response.status(Response.Status.OK).build();
} else {
Log.info("inventory stock validation failed");
return Response.status(Response.Status.NOT_ACCEPTABLE).entity(inventoryStockResult).build();
}
}
@Path("update/item/{orderId}")
@PUT
public Response updateOrderItemQuantity(@PathParam("orderId") Long orderId, OrderItem orderItemForm) {
[1] InventoryStockResultRecord inventoryStockResult = inventoryService.checkStockAvailability(orderItemForm.productUUID,orderItemForm.quantity);
Log.infof(":requested stock validation check from inventory for item: %s. Available quantity is: %s", inventoryStockResult.productUUID(),inventoryStockResult.availableQuantity());
if(inventoryStockResult.available()) {
Log.info("inventory stock validation passed");
[2] InventoryProductRecord inventoryProductResult = inventoryService.findProductByUUID(orderItemForm.productUUID);
Log.infof(":requested details for item with id: %s from inventory",inventoryProductResult.UUID());
Order.updateOrderItemQuantity(orderId,inventoryProductResult.UUID(),orderItemForm.quantity);
return Response.status(Response.Status.OK).build();
} else {
Log.info("inventory stock validation failed");
return Response.status(Response.Status.NOT_ACCEPTABLE).entity(inventoryStockResult).build();
}
}
@Path("delete/item/{orderId}")
@PUT
public Response deleteOrderItem(@PathParam("orderId") Long orderId, OrderItem orderItemForm) {
Order.deleteOrderItem(orderId,orderItemForm.productUUID);
return Response.status(Response.Status.OK).build();
}
@GET
@Path("customer/{id}")
public List<Order> findOrdersByCustomerId(@PathParam("id") Long id) {
return Order.findOrdersByCustomerId(id);
}
@DELETE
@Path("delete/{id}")
public Response deleteOrder(@PathParam("id") Long id) {
Order.deleteOrder(id);
return Response.status(Response.Status.OK).build();
}
}
STEP 5. Implementing the REST Client Reactive Interface for our new inventory-service.
We then create a new interface class to handle the REST calls of our code branch to the inventory micro-service.
This new interface is called IProductInventoryClient within package com.platform.ecommerce.order.rest.
IProductInventoryClient.java
package com.platform.ecommerce.order.rest;
import com.platform.ecommerce.order.model.InventoryProductRecord;
import com.platform.ecommerce.order.model.InventoryStockResultRecord;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
@RegisterRestClient(configKey="inventory-service")
@Path("/inventory-service/v1/product") // The base URL of the REST resource
public interface IProductInventoryClient {
@GET
@Path("stock/availability/{UUID}/{quantity}")
InventoryStockResultRecord checkStockAvailability(@PathParam("UUID") String UUID, @PathParam("quantity") Integer quantity);
@GET
@Path("uuid/{UUID}")
InventoryProductRecord findProductByUUID(@PathParam("UUID") String UUID);
}Lets examine what is going on:
- Declaring the methods: First, we define a Java interface for the REST API of our hypothetical inventory micro-service.
- Annotating the interface:
Second, we are using the
@RegisterRestClientannotation, which is a key annotation used in MicroProfile Rest Client to declare and register a Java interface as a REST client.
Thus we are telling Quarkus to generate an implementation of that interface at runtime and to interact with the REST API of our inventory micro-service.
3. Specify the URL of our inventory micro-service: Within the @RegisterRestClient annotation we specify information about the base URL of the REST API of the inventory micro-service as well as other config parameters using configKey=”inventory-service”.
We also add within application.properties that config-key using the format quarkus.rest-client.{config-key-name}.url
application.properties
quarkus.rest-client.inventory-service.url=http://localhost:80914. Injecting the REST Client:
In our IProductRemoteMicroService we inject an instance of this interface using either the @Inject annotation or other dependency injection mechanisms like the constructor style as shown at line [1].
package com.platform.ecommerce.order.service;
import com.platform.ecommerce.order.model.InventoryProductRecord;
import com.platform.ecommerce.order.model.InventoryStockResultRecord;
import com.platform.ecommerce.order.rest.IProductInventoryClient;
import io.quarkus.arc.profile.UnlessBuildProfile;
import io.quarkus.arc.properties.UnlessBuildProperty;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@ApplicationScoped
@UnlessBuildProperty(name = "microservices.inventory.release.enabled", stringValue = "false")
//@UnlessBuildProfile("prod")
public class ProductRemoteMicroService implements IInventoryService {
IProductInventoryClient inventoryClient;
[1] public ProductRemoteMicroService(@RestClient IProductInventoryClient inventoryClient) {
this.inventoryClient = inventoryClient;
}
@Override
public InventoryProductRecord findProductByUUID(String UUID) {
return inventoryClient.findProductByUUID(UUID);
}
@Override
public InventoryStockResultRecord checkStockAvailability(String UUID, Integer quantity) {
return inventoryClient.checkStockAvailability(UUID, quantity);
}
}STEP 6. Import Product Sample Data with UUIDs.
Within resources directory we change import.sql file to include insert product statements with UUIDs. We also re-written our sample data to include more items and better names and descriptions this time.
We don’t touch stock info since we said aleady that we will turn on and off the new functiionality. Thus we need the local database stock info to coexist with the new functionality of fetching that data from the inventory micro-service.
The import SQL file
import.sql
INSERT INTO customer (id, name, surname,email,address,phone) VALUES ( nextval('customer_seq'), 'Marco','Verratti','[email protected]','Princes Park (Le Parc des Princes)','000000000');
INSERT INTO customer (id, name, surname,email,address,phone) VALUES ( nextval('customer_seq'), 'Kylian','Mbappé','kylian.mbappé@gmail.com','Princes Park (Le Parc des Princes)','000000000');
------------------------------- Products -----------------------------------------------------
-- Apple Product 1
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (1, '4a05b88a-6e9f-4e94-b799-9d032b33d749', 3999, 'Powerful desktop computer for professionals', 'Mac Pro', 75);
-- Apple Product 2
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (2, 'b74fe0a0-5799-4343-9820-251d4b3c49ef', 1999, 'Slim and lightweight laptop with Retina display', 'Mac Book', 100);
-- Apple Product 3
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (3, '7d06a914-4ec3-42d1-821a-21ff8a98e13f', 1999, 'High-performance tablet with Apple Pencil support', 'iPad Pro', 100);
-- Apple Product 4
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (4, 'b3f42c5e-8c7e-4e8f-92f6-104a7a480932', 2999, 'Compact desktop computer for creative professionals', 'Mac Studio', 100);
-- Apple Product 5
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (5, '6a5e89d0-17a6-4b5a-85de-12fb17001f9d', 999, 'Lightweight and versatile iPad', 'iPad Air', 100);
-- Apple Product 6
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (6, '14cfe9e5-5d5e-45d3-8b60-8d89e5cdd96e', 1499, 'Professional tablet with Apple Pencil support', 'iPad Pro', 100);
-- Apple Product 7
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (7, 'a70c22b7-5a0e-4587-92e5-f85572ca075f', 899, 'Latest iPhone with A15 Bionic chip', 'iPhone 13', 100);
-- Apple Product 8
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (8, 'bc4e30d8-5089-4e35-94cc-d305c0906b22', 1099, 'Advanced iPhone with Pro camera system', 'iPhone 13 Pro', 100);
-- Apple Product 9
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (9, '28aa1913-67a3-4929-ba4b-93019e00e3b5', 499, 'Compact iPhone with powerful features', 'iPhone SE', 100);
-- Apple Product 10
INSERT INTO product(id, uuid, price, description, name, stock)
VALUES (10, 'ad0da00c-2633-4dd1-9329-aa9437e208e4', 1299, 'Flagship iPhone with Pro Max camera system', 'iPhone 13 Pro Max', 100);
INSERT INTO users (id, name, password) VALUES ( nextval('users_seq'), 'Kylian','1234');
INSERT INTO users (id, name, password) VALUES ( nextval('users_seq'), 'Marco','1234');
OrderResourceTest.java
package com.platform.ecommerce.order.resource;
import com.platform.ecommerce.order.model.OrderStatus;
import com.platform.ecommerce.order.model.OrderItem;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.junit.jupiter.api.*;
import static io.restassured.RestAssured.given;
@QuarkusTest
@TestHTTPEndpoint(OrderResource.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
//@Disabled
public class OrderResourceTest {
String testProductUUID = "4a05b88a-6e9f-4e94-b799-9d032b33d749";
@Test
@Order(1)
public void createOrder() {
JsonObject order = Json.createObjectBuilder()
.add("status", OrderStatus.Pending).build();
// Test POST
given()
.contentType(MediaType.APPLICATION_JSON)
.body(order.toString())
.when()
.when().post("create/{customerId}",1)
.then()
.statusCode(Response.Status.CREATED.getStatusCode());
}
@Test
@Order(2)
public void addOrderItem() {
JsonObject orderItem = Json.createObjectBuilder()
.add("productUUID", testProductUUID)
.add("quantity", 2)
.build();
// Test POST
given()
.contentType(MediaType.APPLICATION_JSON)
.body(orderItem.toString())
.when()
.when().post("add/item/{orderId}",1)
.then()
.statusCode(Response.Status.OK.getStatusCode());
}
@Test
@Order(3)
public void whenFetchOrder_ThenAtLeastOneOrderShouldBeFound() {
//TEST GET
com.platform.ecommerce.order.model.Order order = given()
.accept(MediaType.APPLICATION_JSON)
.when().get("{id}",1)
.then()
.statusCode(Response.Status.OK.getStatusCode())
.extract()
.body().as(com.platform.ecommerce.order.model.Order.class);
}
@Test
@Order(4)
public void whenUpdateOrderItemQuantity_ThenQuantityShouldBeEqual() {
JsonObject orderItemJSON = Json.createObjectBuilder()
.add("productUUID", testProductUUID)
.add("quantity", 5)
.build();
// Test UPDATE
given()
.contentType(MediaType.APPLICATION_JSON)
.body(orderItemJSON.toString())
.when().put("update/item/{orderId}",1)
.then()
.statusCode(Response.Status.OK.getStatusCode());
//FETCH ORDER
com.platform.ecommerce.order.model.Order order = given()
.accept(MediaType.APPLICATION_JSON)
.when().get("{id}",1)
.then()
.statusCode(Response.Status.OK.getStatusCode())
.extract()
.body().as(com.platform.ecommerce.order.model.Order.class);
//EXTRACT THE ORDER ITEM FOR PRODUCT WITH ID=1
OrderItem orderItem = order.orderItems.stream().filter(o -> o.product.UUID.equalsIgnoreCase(testProductUUID)).findFirst().orElse(null);
Assertions.assertEquals(orderItem.quantity,5);
}
@Test
@Order(5)
public void whenAddingOrderItemGreaterThanStockQuantity_ThenValidationShouldReturn() {
JsonObject orderItem = Json.createObjectBuilder()
.add("productUUID", testProductUUID)
.add("quantity", 100)
.build();
// Test POST
com.platform.ecommerce.order.model.InventoryStockResultRecord availability = given()
.contentType(MediaType.APPLICATION_JSON)
.body(orderItem.toString())
.when()
.when().post("add/item/{orderId}",1)
.then()
.statusCode(Response.Status.NOT_ACCEPTABLE.getStatusCode()).extract()
.body().as(com.platform.ecommerce.order.model.InventoryStockResultRecord.class);
Assertions.assertEquals(availability.missingQuantity(),25);
}
}Then within IntelliJ open up a terminal and hit mvn clean test.
Don’t forget that the feature toggle microservices.inventory.release.enabled should be set to false since we haven’t yet implemented the inventory-service.
Epilogue.
A few thoughts: We invoke twice the REST Service. One for product availability and a second time for fetching products. Of course this is done only for the purpose of this tutorial and in real-life scenario we may use only one invocation both for inventory stock and product info, since our product data comes from only one micro service, the inventory-service.
However let’s say we have the product image served by another micro-service named file-service. Then we need that second invocation. We will explore that scenario in a next article and see how GraphQL with Quarkus comes into play.
GraphQL is a query language for your API that allows clients to request exactly the data they need and nothing more. One of the key advantages of GraphQL is that it enables clients to make multiple related queries in a single request, which can reduce the over-fetching of data and minimize the number of network requests compared to traditional REST APIs.
That’s all for the first part of this tutorial guys. In the next part of this tutorial we are implementing the inventory-service.
TUTORIAL PARTS
PREVIOUS TUTORIAL
Part 1: Quarkus Persistence, CRUD with Panache. E-commerce example.
Part 2: Quarkus Persistence, CRUD with Panache. E-commerce example.