Part 3: Creating BFF microservice architecture using Kubernetes and NodeJs
We are done with our Product and Order Svc and now its time to create our star of the show, The 180 lbs Heavy Weight Champion BFF Svc.
You can clone the code for this blog from here https://github.com/vanshkapoor/Bff-Microservice
For the folks learning, they can try creating user svc by themselves. The deployments would be very similar to Order and Product Svc.
Development Time
BFF Svc will not be having any datastore as it will act as an API Gate way for all the other services.
That means, we will be calling all the routes of other services inside our BFF.
For this, our folder structure will be in this way
Our BFF will be making REST API calls to our other services. This way we can have an interservice communication and the calls will be fast since our services will be residing within the same cluster.
Our Service Directory having service for each microservice will not have any logic. This is like our Repo Layer which will be only used to have CRUD Operations.
Let's see our Product service
const { default: axios } = require("axios");
const { ProductSvcBaseUrl } = require("../config/apiconfig");
class ProductSvc {
constructor(){
this.url = ProductSvcBaseUrl; // Attached our product svc to url
console.log(this.url);
}
async getAll(){
console.log("URL: ", this.url);
const response = await axios.get(`${this.url}/`); // Rest Call
return response.data;
}
async findOne(id){
const response = await axios.get(`${this.url}/${id}`);
return response.data;
}
async createProduct(ProductName, ProductQuantity){
const response = await axios.post(`${this.url}/`, {
ProductName, ProductQuantity
});
return response.data;
}
// This route is for updating our product store quantity when order is placed
async updateQuantity(ProductId, ProductQuantity){
try {
await axios.post(`${this.url}/${ProductId}/quantity`, {
ProductQuantity
});
return;
} catch(e) {
throw new Error("Product quantity update failed")
}
}
}
module.exports = new ProductSvc();
Order Service
const { default: axios } = require("axios");
const { OrderSvcBaseUrl } = require("../config/apiconfig");
class OrderSvc {
constructor(){
this.url = OrderSvcBaseUrl
console.log(this.url);
}
async getAll(){
console.log(this.url);
const response = await axios.get(`${this.url}/`);
return response.data;
}
async findOne(id){
const response = await axios.get(`${this.url}/${id}`);
return response.data;
}
async createOrder(ProductId, UserId, Quantity, Cost){
try{
const response = await axios.post(`${this.url}/`, {
ProductId, UserId, Quantity, Cost
});
return response.data;
} catch (e) {
throw new Error("Unable to create order")
}
}
async deleteOrder(){
const response = await axios.delete(`${this.url}/`);
return response;
}
}
module.exports = new OrderSvc();
Placing an Order
It's now time to write the business logic for placing an order.
For placing an order we need to make sure that our API is consistent. It has to be safe of Race conditions.
We will design the place order API in this flow :
The user req goes to BFF.
Step 1:
The BFF validates if the product is present and we have sufficient quantity.
If Not, we throw back an Error.
const { ProductId, UserId, Quantity, Cost } = req.body;
let product = null;
try {
product = await productSvc.findOne(ProductId); // get product
if(product.data == null) // error if not present
{
throw new Error("Product doesn't exist");
}
if(product.data.ProductQuantity < Quantity) // error if less quantity
{
throw new Error("Product not available");
}
} catch(error){
res.status(400).send({message: error.message});
}
Step 2:
Next we call the Order Svc and place the Order
newOrder = await orderSvc.createOrder(ProductId, UserId, Quantity, Cost);
Step 3:
Once, order is success we will update the product quanitity
await productSvc.updateQuantity(ProductId, product.data.ProductQuantity - Quantity);
Step 4:
If the update quantity fails, we want the order to be cancelled and return an error. This is to keep our database consistent and avoid issues.
try{
newOrder = await orderSvc.createOrder(ProductId, UserId, Quantity, Cost);
await productSvc.updateQuantity(ProductId, product.data.ProductQuantity - Quantity);
res.json({ data: newOrder });
} catch(error){
if(error && error.message == "Product quantity update failed")
{
await orderSvc.deleteOrder(newOrder.data.id); // we revert our order created as quantity update failed
}
res.status(400).send({message: "Unable to place order"});
}
Our final Order Placement route will be this:
router.post("/", async ( req, res ) => {
const { ProductId, UserId, Quantity, Cost } = req.body;
let product = null;
try {
product = await productSvc.findOne(ProductId);
if(product.data == null)
{
throw new Error("Product doesn't exist");
}
if(product.data.ProductQuantity < Quantity)
{
throw new Error("Product not available");
}
} catch(error){
res.status(400).send({message: error.message});
}
let newOrder = null;
try{
newOrder = await orderSvc.createOrder(ProductId, UserId, Quantity, Cost);
await productSvc.updateQuantity(ProductId, product.data.ProductQuantity - Quantity);
res.json({ data: newOrder });
} catch(error){
if(error && error.message == "Product quantity update failed")
{
await orderSvc.deleteOrder(newOrder.data.id); // we revert our order created as quantity update failed
}
res.status(400).send({message: "Unable to place order"});
}
})
You can go through the codebase from this github link.
One Last Dance
Time to deploy the BFF service in kubernetes and make appropriate connection to the other services using their service name and not the IP address.
our BFF Deployment.yml file:
apiVersion: apps/v1
kind: Deployment
metadata:
name: bffsvc-rs
spec:
replicas: 3
selector:
matchLabels:
app: bffsvc
template:
metadata:
labels:
app: bffsvc
spec:
containers:
- name: usercontainer
image: vanshk7/bffsvc
ports:
- containerPort: 3003
env:
- name: PRODUCT_SVC_BASE_URL
value: 'http://productsvc:3001'
- name: ORDER_SVC_BASE_URL
value: 'http://ordersvc:3002'
Now this is important step, if you notice in env variable the values are set to http and pointing to our service names.
and our BFF service.yml file will be
apiVersion: v1
kind: Service
metadata:
name: bffsvc
spec:
selector:
app: bffsvc
ports:
- protocol: TCP
port: 3003
targetPort: 3003
type: NodePort
Its time to test our final big gun BFF service.
Port forwarding BFF service and no other service is port forward now.
Seeing our Product API's
Seeing our Order API's
Placing an Order
Hitting with quantity more than order quantity
-
Hitting with product Id not present
-
Placing correct order and seeing product quantity getting updated.
Awesome and kudos to actually making till here! We have successfully created an end to end microservice BFF architecture with consistent API designs for an E commerce application.
This design is super scalable and efficient for handling good amount of requests and maybe in some other blog we will see how much!
You can fork the code from this github link and read about it more.
Feel free connect with me on my linked and always open to talk more on designing and building scalable backend.
Untill next time, take care and happy coding👻
explore more such blogs here
You can read our previous parts of this series
https://vanshkapoor.hashnode.dev/part-2-creating-bff-architecture-using-kubernetes-and-nodejs
Subscribe to my newsletter
Read articles from vansh Kapoor directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
vansh Kapoor
vansh Kapoor
Developing large scale application for multiple clients in Thoughtworks. Love to share my knowledge in React, Js and clean coding paractices and travel.