Backend Development 12 min read

HTTP Retry Strategies in Offline Store Systems: Simple Loop, Apache HttpClient, and MQ‑Based Asynchronous Retries

This article explores practical HTTP retry solutions for offline store applications, covering a basic loop retry, the built‑in retry mechanism of Apache HttpClient with custom handlers, and an asynchronous retry approach using message queues to achieve higher reliability and eventual consistency.

Architect
Architect
Architect
HTTP Retry Strategies in Offline Store Systems: Simple Loop, Apache HttpClient, and MQ‑Based Asynchronous Retries

In offline store system development, many features require HTTP communication with third‑party services, such as syncing product information to electronic price tags or sending employee clock‑in data to an external EHR system. Network instability often leads to timeouts, prompting the need for robust HTTP retry strategies.

2.1 Simple Retry

The most straightforward method is to wrap the HTTP call in a loop that retries until success or a maximum number of attempts is reached. Example code:

int retryTimes = 3;
for (int i = 0; i < retryTimes; i++) {
    try {
        // request interface code
        break;
    } catch (Exception e) {
        // handle exception
    }
}

This approach works but treats all exceptions uniformly, which can lead to blind retries.

2.2 Apache HttpClient Retry Mechanism

Apache HttpClient provides a built‑in retry mechanism. The typical usage involves creating a client, executing a request, and handling the response:

CloseableHttpClient httpClient = HttpClientBuilder.create().build();
HttpGet httpGet = new HttpGet("url");
CloseableHttpResponse response = httpClient.execute(httpGet);
HttpEntity entity = response.getEntity();

During client creation, HttpClientBuilder builds a RetryExec with a RetryHandler . By default, RetryExec retries on IOException using DefaultHttpRequestRetryHandler , which retries up to three times unless the request is non‑idempotent or the exception belongs to a non‑retriable list.

if (!automaticRetriesDisabled) {
    HttpRequestRetryHandler retryHandlerCopy = this.retryHandler;
    if (retryHandlerCopy == null) {
        retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE;
    }
    execChain = new RetryExec(execChain, retryHandlerCopy);
}

The core retryRequest method checks the execution count, non‑retriable exception classes, request idempotency, and whether the request has already been sent:

public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
    if (executionCount > this.retryCount) return false;
    if (this.nonRetriableClasses.contains(exception.getClass())) return false;
    if (handleAsIdempotent(request)) return true;
    if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) return true;
    return false;
}

By default, four exception types (InterruptedIOException, UnknownHostException, ConnectException, SSLException) are excluded from retries, and POST requests are not retried because they are non‑idempotent.

2.3 Asynchronous Retry via Message Queue

To decouple retry logic from business processing, an MQ consumer can perform the HTTP call. If the call fails, the MQ’s built‑in retry (e.g., RocketMQ’s exponential back‑off) handles subsequent attempts, ensuring eventual consistency without blocking the main flow.

The combined solution adopts both synchronous retries (using HttpClient’s retry handler) and asynchronous retries (via MQ), providing up to 51 total attempts (3 synchronous + 16 MQ retries per attempt). This hybrid approach eliminated complaints about delayed price‑tag updates or missed clock‑in synchronizations.

Customizing the HttpClient retry handler resolves the default limitations (no POST retries, SocketTimeoutException not retried). The custom handler adds logic for specific exceptions and ensures only idempotent requests are retried when appropriate:

public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
    if (executionCount > this.retryCount) {
        return false;
    } else if (exception instanceof NoHttpResponseException) {
        return true;
    } else if (exception instanceof SSLHandshakeException) {
        return false;
    } else if (exception instanceof InterruptedIOException) {
        return true;
    } else if (exception instanceof UnknownHostException) {
        return false;
    } else if (exception instanceof ConnectTimeoutException) {
        return false;
    } else if (exception instanceof SSLException) {
        return false;
    } else {
        HttpClientContext clientContext = HttpClientContext.adapt(context);
        HttpRequest request = clientContext.getRequest();
        return !(request instanceof HttpEntityEnclosingRequest);
    }
}

Integrating the custom handler is straightforward:

CloseableHttpClient httpClient = HttpClientBuilder.create()
    .setRetryHandler(StoreRequestRetryHandler.INSTANCE)
    .build();

Overall, the hybrid retry strategy improves reliability, reduces latency for real‑time calls, and leverages MQ back‑off to handle temporary third‑party outages.

BackendJavaretryMessage QueueHTTPresilienceApache HttpClient
Architect
Written by

Architect

Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.

0 followers
Reader feedback

How this landed with the community

login 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.