Sobes.tech
Senior

There is a backend, there is a UI. The stack is standard: Java, Spring, React, Postgres. Task: Design a REST endpoint that takes data from 3 sources and returns it to the UI, while ensuring maximum throughput and minimal response time (max/min criteria based on our specific conditions, since there is no silver bullet and we need to find a compromise). It is known that the expected peak RPS for this REST endpoint will be 200. Details about sources: 1st source - our database, query takes about 15 seconds. 2nd source - a related REST service, degrades to 2 minutes at 100 RPS, normally responds in 5 seconds. We cannot change this source's behavior. 3rd source - another related REST service, randomly (no pattern found) returns 503, normally responds in 10 seconds. We cannot change this source's behavior.

sobes.tech AI

Answer from AI

For designing a REST endpoint with three data sources, considering throughput and response time requirements, I propose the following approach:

  1. Asynchronous parallel requests to sources:

    • Requests to the database and two REST services are executed in parallel to minimize overall waiting time.
  2. Caching:

    • For the source with a long database query (15 seconds), implement caching of results with a reasonable TTL to reduce load during peak requests.
  3. Timeouts and fallback:

    • For the second source, which degrades under high load, set timeouts (e.g., 5-7 seconds) and use cache or return partial data if the service does not respond.
    • For the third source, which occasionally returns 503, implement retries with exponential backoff and fallback to cache or default data.
  4. Reactive programming:

    • Use Spring WebFlux or CompletableFuture for non-blocking execution of requests and aggregation of results.
  5. Data aggregation and response:

    • After receiving data from all sources, aggregate them into a single response.
    • If any source is unavailable, return partial data with a status indicator.
  6. Monitoring and load limiting:

    • Implement a circuit breaker (e.g., Resilience4j) to protect against degradation of related services.
    • Limit the number of concurrent requests to problematic sources.

Example of using CompletableFuture for parallel requests:

CompletableFuture<Data1> future1 = CompletableFuture.supplyAsync(() -> fetchFromDb());
CompletableFuture<Data2> future2 = CompletableFuture.supplyAsync(() -> fetchFromService2());
CompletableFuture<Data3> future3 = CompletableFuture.supplyAsync(() -> fetchFromService3());

CompletableFuture.allOf(future1, future2, future3).join();

// Process results considering errors and timeouts
Data1 d1 = future1.getNow(null);
Data2 d2 = future2.getNow(null);
Data3 d3 = future3.getNow(null);

// Form the response
return aggregate(d1, d2, d3);

This approach balances response speed and reliability, taking into account the limitations and characteristics of each source.