How I Built a High‑Performance Java Price‑Comparison Engine from Scratch

Starting from a simple sequential Java price‑aggregator, the article walks through successive architectural upgrades—concurrent calls with CompletableFuture, timeout and fallback handling, Spring Boot service exposure, caching, bulkhead isolation, microservice split, and Kafka‑driven event processing—showing how latency drops from 1500 ms to under 20 ms.

LuTiao Programming
LuTiao Programming
LuTiao Programming
How I Built a High‑Performance Java Price‑Comparison Engine from Scratch

Original Version: Single‑Threaded Serial Calls

The naive implementation creates a class that calls each vendor’s API one after another:

package com.icoderoad.aggregator.simple;

public class PriceAggregator {
    public static void main(String[] args) {
        VendorClient amazon = new AmazonClient();
        VendorClient flipkart = new FlipkartClient();
        VendorClient walmart = new WalmartClient();
        List<VendorClient> vendors = List.of(amazon, flipkart, walmart);
        for (VendorClient vendor : vendors) {
            PriceResponse price = vendor.getPrice("iphone-15");
            System.out.println(vendor.getName() + " -> " + price.getPrice());
        }
    }
}

Problems identified:

Requests are executed serially.

Total latency equals the sum of all vendor latencies.

A single slow vendor drags the whole response down.

Example timings: Amazon 400 ms, Flipkart 500 ms, Walmart 600 ms → total 1500 ms, which is unacceptable for users.

Step 1: Concurrent Requests with CompletableFuture

IO is parallelised using a fixed‑size thread pool and CompletableFuture:

package com.icoderoad.aggregator.async;

ExecutorService executor = Executors.newFixedThreadPool(10);

CompletableFuture<PriceResponse> amazon = CompletableFuture.supplyAsync(() -> amazonClient.getPrice(product), executor);
CompletableFuture<PriceResponse> flipkart = CompletableFuture.supplyAsync(() -> flipkartClient.getPrice(product), executor);
CompletableFuture<PriceResponse> walmart = CompletableFuture.supplyAsync(() -> walmartClient.getPrice(product), executor);

List<PriceResponse> prices = CompletableFuture
    .allOf(amazon, flipkart, walmart)
    .thenApply(v -> List.of(amazon.join(), flipkart.join(), walmart.join()))
    .join();

Now total latency equals the slowest vendor (600 ms) instead of the sum, a first "quality jump".

Step 2: Fault Tolerance (Timeout + Fallback)

Real‑world third‑party APIs often fail, so a timeout and fallback are added:

package com.icoderoad.aggregator.resilience;

CompletableFuture<PriceResponse> amazon = CompletableFuture
    .supplyAsync(() -> amazonClient.getPrice(product), executor)
    .completeOnTimeout(defaultPrice(), 800, TimeUnit.MILLISECONDS)
    .exceptionally(ex -> fallbackPrice());

Timeout protects threads from being blocked indefinitely.

Exception handling guarantees a response even when a vendor fails.

Partial success is acceptable; the system does not require every vendor to succeed.

At this stage the application is still a monolith but achieves production‑grade availability.

Service Exposure with Spring Boot

The aggregator is turned into a REST API:

package com.icoderoad.aggregator.controller;

@RestController
@RequestMapping("/prices")
public class PriceController {
    private final PriceAggregatorService service;

    @GetMapping("/{product}")
    public List<PriceResponse> getPrices(@PathVariable String product) {
        return service.getPrices(product);
    }
}

Connection pooling reduces resource overhead.

Asynchronous HTTP client (WebClient) is introduced.

API contract is standardized.

Introducing Cache to Reduce Cost and Latency

Because third‑party calls are slow and costly, results are cached:

package com.icoderoad.aggregator.cache;

@Cacheable(value = "prices", key = "#product")
public List<PriceResponse> getPrices(String product) {
    return fetchFromVendors(product);
}

Latency drops from 600 ms to about 20 ms, a second "quality jump".

Isolation and Rate Limiting

Stability becomes the focus. The following strategies are applied:

Thread‑pool isolation (Bulkhead).

Circuit‑breaker using Resilience4j.

Rate‑limiting controls.

Example configuration:

Amazon   -> thread pool 10
Flipkart -> thread pool 10
Walmart  -> thread pool 10

Any single vendor failure no longer impacts the whole system.

Splitting into Microservices

When traffic grows, the monolith becomes a bottleneck. The system is decomposed so each vendor integration lives in its own service, fully decoupling authentication, data formats, and rate‑limit rules.

Microservice decomposition diagram
Microservice decomposition diagram

Event‑Driven Architecture with Kafka

To avoid querying vendors on every user request, price collection is shifted to a background pipeline:

Pre‑collect prices periodically.

Publish price updates as Kafka events.

Sample event payload:

{
  "productId": "iphone-15",
  "vendor": "amazon",
  "price": 799,
  "timestamp": "2026-03-09T10:30:00"
}

Benefits:

Queries no longer depend on external APIs.

Latency falls to 5‑20 ms.

Overall system stability improves dramatically.

Event‑driven flow diagram
Event‑driven flow diagram

Final Architecture: High‑Availability Distributed System

Final system diagram
Final system diagram

Strong horizontal scalability.

Ultra‑low latency.

Complete vendor decoupling.

Real‑time data updates.

High fault tolerance.

Core Challenges of a Mature Price‑Comparison System

Fan‑out: a single request may need to call dozens or hundreds of vendor APIs.

Tail latency: the slowest vendor determines overall response time.

Unstable third‑party services: failures are the norm, not the exception.

Data freshness: prices change frequently.

Cost control: more calls mean higher operational cost.

Evolution Path Summary

The design progresses step by step:

Serial calls
→ Concurrent calls
→ Fault‑tolerance mechanisms
→ Cache optimization
→ Service decomposition
→ Event‑driven architecture

This trajectory reflects engineering mindset rather than mere technology selection.

Conclusion

Repeatedly refining a simple price‑lookup feature reveals that system complexity is driven by real‑world constraints. Transforming a basic comparator into a high‑concurrency, distributed platform illustrates the true engineering threshold: making the system "fast, stable, and scalable".

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavamicroservicesconcurrencyCachingKafkaSpring BootResilience4jPrice Aggregation
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.