In my previous blog post Chopping the monolith, I explained my stance on microservices and why it shouldn't be your golden standard. However, I admitted that some parts of the codebase were less stable than others and had to change more frequently. I proposed "chopping" these few parts to cope with this requirement while keeping the monolith intact. As Linus Torvalds once wrote:
Talk is cheap, show me the code!
I want to show how to do it within the scope of a small demo project to comply with the above statement.
The use-case: pricing
In my career, I've spent some years in the e-commerce domain. E-commerce in the real world is much more complex than people might think. Yet, I found that simplifications of some parts of e-commerce are easy to understand because it "speaks" to the audience.
A huge chunk of e-commerce is dedicated to pricing. Pricing rules are very volatile and need to change quite frequently. Here are some reasons:
Too much stock of a specific product
End of season: the new collection has arrived, and we need to make room in the shop (or the warehouse)
Studies show that decreasing the price (and thus the margin) of a product will increase sales of this product so that the company will earn more money overall
Marketing purposes: for example, a product prominently branded with the company logo
etc.
Here, we have an e-commerce shop:
We can add items to the cart and check its content:
The initial situation
The following diagram models the existing flow:
The application relies on the Spring Boot framework: it's coded in Kotlin and uses the Beans and Routers DSLs. It leverages Kotlin's coroutines to implement asynchronous communication.
Only sum up the prices of each product separately; it's a demo, after all
At this point, pricing is strongly coupled to the CheckoutHandler.
Chopping pricing
Before using an alternative pricing service, we have to chop the pricing service by moving it to its dedicated route. The new flow is the following:
The new architecture includes a couple of changes:
Pricing is exposed as a dedicated route
The view doesn't return the price anymore
The client orchestrates the flow between the checkout and pricing routes
The new code reflects this:
funprice(checkout:CheckoutView):Double{println("Pricing computed from the monolith")returncheckout.lines.fold(0.0){current,line->current+line.first.price*line.second}}classPricingHandler{suspendfuncompute(req:ServerRequest):ServerResponse{valcart=req.bodyToMono<CheckoutView>().awaitSingle()valprice=price(cart)returnServerResponse.ok().bodyValueAndAwait(price)}}funpricingRoute()=coRouter{valhandler=PricingHandler()POST("/price",handler::compute)}
Opening the browser dev tools reveals both HTTP requests on the checkout page:
Status
Method
Domain
File
Initiator
Type
200
GET
localhost:9080
c
checkout:1
json
200
POST
localhost:9080
price
checkout:1
plain
Using an alternative pricing service
At this stage, if we decide to use an alternative pricing feature, we would have to deploy an updated version of the application with the client calling the alternative URL. Each change to the pricing alternative may require a new deployment. Since the idea is to keep the deployed monolith, we shall improve the architecture instead.
Real-world architectures rarely expose their backend services directly to the outside world. Most, if not all, organizations hide them behind a single entry-point; a Reverse Proxy.
However, Reverse Proxies are rigid regarding configuration in general and route configuration in particular. For flexibility reasons, one may be inclined to use an API Gateway. For example, the Apache APISIX API Gateway allows changing route configuration on the fly via its REST API.
I've prepared a Microsoft Azure Function where I uploaded the pricing code implemented in JavaScript:
module.exports=asyncfunction (context,req){context.log('Pricing computed from the function')constlines=req.body.linescontext.log(`Received cart lines: ${JSON.stringify(lines)}`)constprice=lines.reduce((current,line)=>{returncurrent+line.first.price*line.second},0.0)context.log(`Computed price: ${price}`)context.res={body:price}context.done()}
With Apache APISIX, we can configure the two routes above.
Configure the pricing route to use the Azure Function
Apache APISIX provides a plugin that integrates natively with Azure Functions
Function's URL
Function's secret key
At this point, while the monolithic shop contains pricing code, it's never called. We can plan to retire it during the next release.
On the other side, we can update the pricing logic according to new business requirements without deploying anything but the function itself.
Conclusion
My previous post focused on why to use microservices and, more importantly, why not to use them. The reason is to speed up the pace of deployment of some parts of the code. Instead of microservices, we can isolate these parts in a dedicated Function-as-a-Service.
In this post, I tried to go beyond the theory and show how you could achieve it concretely. It boils down to exposing the to-be-chopped part via HTTP and using an API Gateway to route the wanted requests to one's service of choice.
The complete source code for this post can be found on Github: