Spring Boot / Advanced: observability and resilient releases / Lesson 15
Outbound HTTP and resilience budgets
Bound a Spring HTTP client, classify remote outcomes, and choose retries and fallbacks from operation semantics rather than generic exception handling.
On this page
Lesson goals
- Configure deliberate connection/read timeouts.
- Keep retry safety separate from transport failure.
- Preserve trust, observability, and resource budgets.
A remote call is a separate failure boundary
Define the remote capability, permitted base address, authentication, request/response representation, timeout budget, and error mapping before choosing a client. Spring Framework6.2 offers RestClient for synchronous HTTP and other clients for different architectures. Do not mix APIs from a newer Framework line into the Boot3.5 baseline. A client object existing in the context is not proof that the remote dependency is available.
A remote write can complete before its response is lost. A timeout therefore does not automatically prove rollback or safe repetition. Keep downstream failure distinct from a valid empty result or zero price. The application must preserve business uncertainty instead of inventing data that lets the happy path continue.
Configure the selected underlying request factory
The fragment supplies explicit connect and read timeouts through a JDK-backed request factory and uses Boot's injected RestClient.Builder rather than assuming an independently constructed builder has all Boot customizations. The reserved example.test address is illustrative and never contacted. The fragment remains non-runnable: no HTTP client or application context is constructed here.
Timeout stages differ: DNS, connection acquisition, connect, TLS, read, and the complete business deadline can have different controls. Three attempts each using a long timeout can exceed the caller's budget. Match the client backend's actual semantics and test with a controlled fixture when authorized; no remote service, listener, or load test is provisioned for this course.
package training.catalog;
import java.net.http.HttpClient;
import java.time.Duration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
@Configuration(proxyBeanMethods = false)
class InventoryClientConfiguration {
@Bean
RestClient inventoryClient(RestClient.Builder builder) {
var transport = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(1)).build();
var factory = new JdkClientHttpRequestFactory(transport);
factory.setReadTimeout(Duration.ofSeconds(2));
return builder.baseUrl("https://inventory.example.test")
.requestFactory(factory).build();
}
}
The selected backend and timeouts are declared, but no DNS, TLS, HTTP request, context, or external dependency is exercised.
Evaluate repeatability before retry count
The JDK-only model permits a bounded retry for selected GET/HEAD responses or an explicitly established server idempotency contract. It rejects an unsafe POST replay and stops at the third-attempt boundary. This is a small policy model, not a Spring retry library or evidence that a header alone makes a remote mutation idempotent. Its exact Boolean outputs are locally testable.
Use backoff, jitter, total deadlines, cancellation, and concurrency limits in an actual implementation. Classify the exception/status and whether the remote operation could already have completed. A circuit breaker can stop repeated calls to a failing dependency but does not replace timeouts, capacity limits, or correctness. Choose compatible libraries explicitly; retry annotations from another version are not automatically core Boot behavior.
public class Main {
static boolean retry(String method,boolean establishedIdempotency,int attempt,int status) {
boolean repeatable=method.equals("GET")||method.equals("HEAD")||establishedIdempotency;
return attempt<3 && repeatable && (status==429||status==503);
}
public static void main(String[] args) throws Exception {
System.out.println(retry("GET",false,1,503));
System.out.println(retry("POST",false,1,503));
System.out.println(retry("POST",true,3,503));
}
}
true
false
false
The chosen policy returns true,false,false. It performs no HTTP calls and does not establish any real server idempotency guarantee.
Keep remote data and destination trust explicit
Choose a trusted base address and approved operation paths rather than accepting an arbitrary caller URL as a generic proxy. Bind or encode URI values through the client API and validate the domain input. Preserve TLS certificate validation and credential lifecycle. Do not disable validation to suppress an error or log Authorization headers while tracing a failed request.
retrieve-style calls can raise HTTP error exceptions; map expected remote responses deliberately instead of returning null for all failures. Validate response size, media type, required fields, and business meaning. An unavailable inventory source must not silently become zero available stock or infinite availability unless that is an explicit justified product policy. Preserve a restricted cause for diagnosis.
Test the boundary with controlled responses
Use a compatible mock request factory/server fixture or another approved isolated approach to verify URI construction, headers, body conversion, status mapping, deadlines, and retry count without real services. Distinguish a mocked successful body from actual dependency health. Verify cancellation and exhausted attempts leave the application with a truthful failure and no duplicated irreversible side effect.
Correlate client timing and outcome through Boot-aware instrumentation with bounded labels, and retain secret-safe request identity. Avoid layered retries in several components that multiply attempts unexpectedly. The complete resilience contract states which calls repeat, why repetition is safe, how long the operation may take, which fallback is truthful, and how uncertain remote completion is reconciled.
Common errors and fixes
A timeout duplicated a remote write
- Why it happens
- Transport uncertainty was treated as confirmed failure.
- How to fix it
- Reconcile by an established operation identity before replay.
- Prevent it next time
- Test lost-response scenarios.
A fallback invented a valid-looking price
- Why it happens
- Dependency failure was converted into domain data.
- How to fix it
- Use an explicit unavailable/rejected outcome or justified fallback.
- Prevent it next time
- Test failure separately from zero/empty success.
Client timing ignored configured timeout
- Why it happens
- The option belonged to another transport stage/backend.
- How to fix it
- Inspect the actual request factory and total deadline model.
- Prevent it next time
- Verify controlled slow/failure responses when authorized.
Check your understanding
1. Why is a retry count of3 insufficient as a resilience policy?
Show worked solution
It says nothing about repeatability, total deadline, per-attempt timeout, backoff, cancellation, overload, or uncertain completion. Define those together and verify actual attempt multiplication across layers; three unsafe writes remain unsafe.
2. Does a mock response containing a valid product prove the real endpoint is available and authorized?
Show worked solution
No. It can validate selected request/response mapping under the fixture. Real connectivity, credentials, certificates, provider policy, and deployment health need separate authorized evidence. Never relabel fixture output as an observed external call.
Key takeaways
- Timeout is not proof that a remote operation did not complete.
- Retries need semantic safety and a total work budget.
- A truthful fallback and controlled evidence protect application correctness.