Part 2 — Implementing OAuth Token Caching in a Spring Boot API Client

Part 2 — Implementing OAuth Token Caching in a Spring Boot API Client

In the previous article of this series, we explored how to structure a production-grade API client in Spring Boot.

We discussed:

  • Separating external integrations into client modules
  • Centralizing HTTP configuration
  • Using Spring's RestClient
  • Exposing clean service interfaces

However, real-world APIs rarely accept anonymous requests. Most production APIs require OAuth authentication.

And that’s where many integrations start to become inefficient.


The Problem with Naive OAuth Implementations

A common mistake is fetching a token for every API request.

Example:

String token = fetchAccessToken();

restClient.post()
    .uri("/validate")
    .header("Authorization", "Bearer " + token)
    .body(request)
    .retrieve();
        

At first glance, this seems fine. But in production, it causes serious issues: unnecessary token requests, increased latency, API rate limits, and authentication bottlenecks are the key ones.

If your system sends 100 API calls per second, this approach may generate 100 token requests per second as well.

Note that most OAuth providers expect tokens to be reused until they expire.

So the real solution is token caching.


Understanding the OAuth Client Credentials Flow

Many service-to-service APIs use the OAuth Client Credentials flow.

The sequence looks like this:

Application
     │
     ▼
Request Access Token
     │
     ▼
OAuth Server
     │
     ▼
Access Token (expires in ~10 minutes)
     │
     ▼
Use token for API requests        

Once the token expires, the application must refresh it.

The challenge is implementing this in a way that is efficient, thread-safe, and most importantly, reusable across the client


The Interceptor Pattern

A clean solution is to inject the token automatically using a request interceptor.

Architecture:

Service
   │
   ▼
RestClient
   │
   ▼
Token Interceptor
   │
   ▼
External API        

The interceptor ensures that every request contains a valid token, the tokens are cached until expiration, and refresh happens automatically.

Your application code never needs to worry about authentication.


Implementing a Token Cache

A simple token cache needs to store:

  • The access token
  • The expiration time

Example:

AtomicReference<String> cachedToken = new AtomicReference<>();
AtomicLong tokenExpiresAt = new AtomicLong(0);        

Before each request, we check whether the token is still valid.

Current Time
      │
      ▼
Token Expired?
      │
 ┌────┴─────┐
 │          │
No         Yes
 │          │
Use token  Fetch new token        

This avoids unnecessary authentication calls.


Implementing the Token Interceptor

Here is a simplified interceptor example.

@Bean
public ClientHttpRequestInterceptor oauthTokenInterceptor() {

    AtomicReference<String> cachedToken = new AtomicReference<>();
    AtomicLong expiresAt = new AtomicLong(0);

    return (request, body, execution) -> {

        long now = System.currentTimeMillis();

        if (cachedToken.get() == null || now >= expiresAt.get()) {

            TokenResponse token = fetchAccessToken();

            cachedToken.set(token.getAccessToken());
            expiresAt.set(now + token.getExpiresIn() * 1000);
        }

        request.getHeaders().setBearerAuth(cachedToken.get());

        return execution.execute(request, body);
    };
}
        

Now every request automatically includes the bearer token.


Adding a Safety Buffer

Tokens usually expire after a fixed time.

However, network delays or clock differences can cause edge cases where a token expires during a request.

A simple improvement is adding a refresh buffer.

Example:

private static final long TOKEN_REFRESH_BUFFER = 30000;        

Tip: Instead of refreshing exactly at expiry, refresh 30 seconds earlier. This prevents edge-case authentication failures.


Where the Interceptor Fits in the Client

Your final client configuration might look like this:

@Bean
public RestClient externalApiRestClient(
        RestClient.Builder builder,
        ClientHttpRequestInterceptor oauthInterceptor) {

    return builder
            .baseUrl("https://api.example.com")
            .requestInterceptor(oauthInterceptor)
            .build();
}        

Now the authentication logic is completely decoupled from your business services. Your service layer remains clean.


A Hidden Problem: Token Refresh Race Conditions

There is one subtle issue to be aware of. Imagine this situation:

Thread A → token expired
Thread B → token expired
Thread C → token expired        

All three threads might attempt to refresh the token simultaneously. This is sometimes called a token refresh storm. For low-volume systems, this may not matter.

But high-throughput systems may need:

  • Synchronization
  • Distributed caching
  • Centralized token services

We'll explore this topic in a later article in this series.


Production Checklist for OAuth Clients

A robust OAuth integration should, at a minimum, include token caching, expiration tracking, refresh buffers, interceptor-based injection, and centralized configuration.

Without these, your integration may generate unnecessary authentication traffic.


Final Thoughts

OAuth authentication is often treated as a small implementation detail. But in production systems, it becomes a core part of API reliability and performance.

By caching tokens and injecting them through interceptors, you can build API clients that are efficient, scalable, and easy to maintain.

But authentication is only one part of building production-grade integrations. Another equally important aspect is testability.

External APIs introduce network dependencies, rate limits, and unpredictable failures — which means poorly designed clients often lead to fragile or slow tests.

In the next article in this series, we'll explore how to design API clients that are easy to test, without relying on real external services. Because production-grade integrations require production-grade testing strategies.



To view or add a comment, sign in

More articles by Simanta Sarma

Others also viewed

Explore content categories