Microservices – Lessons learned – Part 2

In part 1 of this article, I’ve covered how migrating to microservices inherently requires some organizational changes. Today, I will describe some of the traps I witnessed at clients who undertook such effort for the first time.

A long way to the greenery

Passthrough microservices

One of the common patterns I discovered with clients struggling with this migration process is to build microservices that simply delegate processing business logic to a downstream system (In most cases, it was the monolith). Not only does this approach violates the principle of bounded context in DDD, it also adds more complexity to your SDLC (Software Development Life Cycle). For instance, you will have to deal with the following concerns:

  • Additional deployments
  • Harder debugging process
  • Additional network latency

As an example, I came across an account-service that does nothing but delegates all user management actions to the monolith. A better approach would be to move the whole business logic to this component. However, if the monolith still contains some logic that relies on the user data, you may leverage eventual consistency to maintain a local copy of this data.

One of the biggest challenges in the transition to Service Oriented Architectures is getting programmers to understand they have no choice but to understand both the “then” of data that has arrived from partner services, via the outside, and the “now” inside of the service itself.

Pat Helland

There are a couple of techniques to accommodate similar scenarios (I will introduce a technique in the last section), and I would love if you can use the comments section to share which strategy you used until the migration is fully done.

Authorization: Identity verification

Taking into account the scenario introduced in the previous section, I will explain one of the side effects of this approach that added unnecessary overhead to all microservices that require authenticated requests.

To give you context: I was stuffed at one of the biggest retailers in North America when I had to collaborate with its internal team to modernize their Blue Martini based e-commerce application (which was years after its end-of-life).

Blue Martini is java e-commerce platform that relies on session based authentication. As account-service was a passthrough to Blue Martini, the team has built an endpoint to verify if the user is authenticated, which was consumed by every microservice that requires authenticated requests. This verification was done in a synchronous fashion that created additional overhead and latency to all endpoints. To alleviate this problem, we moved the authentication logic from Blue Martini to account-service and introduced JWT as stateless authentication mechanism. This resulted in removing the unnecessary calls to the verification endpoint, and in achieving higher throughput.

Async vs Sync communication

As you start thinking about building microservices, one of questions that arises is: What is the best why to make them communicate with each other? Should we rely on async or sync communication? The obvious answer to these questions is: It depends!

While microservices are a good fit for asynchronous communication, the microservice logic is better performed in a synchronous way.

Let’s imagine a hypothetical scenario of showing a product details page that’s backed by a product-detail-service, which relies on product-service, price-service, promotion-service, and recommendation-service to build this page.

Product details page

Let’s image that each microservice requires 100ms to process the request. In a sequential fashion, this would require 400ms to compose the product details page response.

getProductDetails()
.then(getProductPrice)
.catch(err)
.then(getProductPromo)
.catch(err)
.then(getProductRecommendation)
.catch(err)

With modern programming languages that have done a great job in supporting parallel and asynchronous programming, it’s now easy to compose the response in 100ms (or up to the same response time of the slowest service).

CompletableFuture<ProductDetail> productDetail = CompletableFuture.supplyAsync(() -> this.productDetailClient.get(productId));
CompletableFuture<ProductPrice> productPrice = CompletableFuture.supplyAsync(() -> this.productPriceClient.get(productId))
CompletableFuture<ProductPromotion> productPromotion = CompletableFuture.supplyAsync(() -> this.productPromotionClient.get(productId)); 
CompletableFuture<ProductRecommendation> productRecommendation = CompletableFuture.supplyAsync(() -> this.productRecommendationClient.get(productId));
CompletableFuture<Void> productPageDetails = CompletableFuture.allOf(productDetail, productPrice, productPromotion, productRecommendation);
productPageDetails.get();

It’s worth mentioning that this pattern is more suitable to query state. Additionally, to tolerate failure it’s recommended to use the circuit-breaker pattern. You can learn more by reading Building Microservices book by Sam Newman.

Finally, in the scenario where microservices need to collaborate in processing a request that involves state change, event-based communication is the recommended way to ensure eventual consistency between services. One of the most popular techniques is to rely on event sourcing and CQRS. You can find more about this topic here.