Apache APISIX
Published in

Apache APISIX

Chopping the monolith — the demo

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.

The code is implemented as the following:

internal fun Cart.toCheckout() = CheckoutView(this, price(this))class CheckoutView(private val cart: Cart, val total: Double) {
val lines: List<Pair<Product, Int>>
get() = cart.content.entries.map { it.toPair() }
}
class CheckoutHandler(private val catalog: Catalog) {
suspend fun fetchCheckout(req: ServerRequest) =
ServerResponse.ok().bodyValueAndAwait(req.cart().toCheckout())
}
fun checkoutRoutes(catalog: Catalog) = coRouter {
val handler = CheckoutHandler(catalog)
GET("/checkout/c", handler::fetchCheckout)
}

The pricing logic is coded in its file:

fun price(cart: Cart): Double {
return cart.content.entries // 1
.fold(0.0) { current, entry ->
current + entry.key.price * entry.value
}
}
  1. 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:

fun price(checkout: CheckoutView): Double {
println("Pricing computed from the monolith")
return checkout.lines
.fold(0.0) { current, line ->
current + line.first.price * line.second
}
}
class PricingHandler {
suspend fun compute(req: ServerRequest): ServerResponse {
val cart = req.bodyToMono<CheckoutView>().awaitSingle()
val price = price(cart)
return ServerResponse.ok().bodyValueAndAwait(price)
}
}
fun pricingRoute() = coRouter {
val handler = PricingHandler()
POST("/price", handler::compute)
}

Opening the browser dev tools reveals both HTTP requests on the checkout page.

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 = async function (context, req) {
context.log('Pricing computed from the function')
const lines = req.body.lines
context.log(`Received cart lines: ${JSON.stringify(lines)}`)
const price = lines.reduce(
(current, line) => { return current + 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.

curl -v -i http://apisix:9080/apisix/admin/routes/1 -H 'X-API-KEY: xyz' -X PUT -d '
{
"uri": "/*", # 1
"upstream": {
"type": "roundrobin",
"nodes": {
"chopshop:8080": 1
}
}
}'
  1. Configure the generic catch-all route
curl -v -i http://apisix:9080/apisix/admin/routes/2 -H 'X-API-KEY: xyz' -X PUT -d '
{
"methods": ["POST"],
"uris": ["/price"], # 1
"plugins": {
"azure-functions": { # 2
"function_uri": "https://chopshoppricing.azurewebsites.net/api/HttpTrigger", # 3
"authorization": {
"apikey": "'"$AZURE_FUNCTION_KEY"'" # 4
},
"ssl_verify": false
}
}
}'
  1. Configure the pricing route to use the Azure Function
  2. Apache APISIX provides a plugin that integrates natively with Azure Functions
  3. Function’s URL
  4. 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.

Originally published at A Java Geek on May 22nd, 2022

--

--

--

Apache APISIX provides rich traffic management features like Load Balancing, Dynamic Upstream, Canary Release, Circuit Breaking, Authentication, Observability, and more…

Recommended from Medium

Copyright and Image Attribution

3 Tricky Python Questions — Pattern Printing Practices

Web hosting doesn’t have to be a nightmare — Introducing Lyrical Host

Documents into Malwareverse : CVE-2022–30190 PoC, Log Analysis and Mitigation

How do Databases Compare for Serverless Functions and Containers?

Flash Stock Firmware on Samsung Galaxy C9 Pro SM-C900F

Flash Stock Rom on Samsung Galaxy

Accessing Jupyter on a Remote Machine Using MobaXTerm and ngrok via a Jump Server

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Nicolas Fränkel

Nicolas Fränkel

Dev Advocate for Apache APISIX. Former developer and architect. Still teaching, learning and blogging.

More from Medium

Concurrency

My summary of jPrime 2022

Why I use Kotlin Instead Java

Kotlin/Gradle/JUnit starter project