Part 3: The Interceptor Pattern - Mastering Transport & Concurrency

 A Staff-level guide to high-concurrency Android networking: Implementing single-flight token refreshes with OkHttp and Kotlin Mutex.

The Interceptor Pattern - Mastering Transport & Concurrency

In Part 1: The Stateless Blueprint and Part 2: The Android Vault, we designed and stored our secure tokens. Now comes the Staff-level operational challenge: The Transport Layer.

When scaling to 5 million users, your networking layer must handle high-concurrency environments without collapsing. If a token expires while 10 parallel requests are in flight, you face a “Refresh Storm.” Today, we build a Networking Command Center using OkHttp, Kotlin Mutex, and the Single-flight refresh pattern.

⚡ TL;DR

  • Stateless auth scales better by removing session-stickiness on the backend.
  • Store tokens using hardware-backed Android Keystore to prevent extraction.
  • Use OkHttp + Mutex to prevent “Refresh Storms” and race conditions.
  • Implement Single-flight refresh to ensure only one network call updates the session.

🛑 The Problem: The “Refresh Storm” and Race Conditions

Most Android authentication best practices warn against simple 401 handling. If your Home Screen triggers 5 parallel API calls (Profile, Balance, Feed, etc.) and the Access Token is expired, all 5 calls simultaneously hit your auth server.

This leads to:

  1. Resource Waste: Slamming your backend with redundant refresh requests.
  2. Rotation Conflicts: Many backends use strict refresh token rotation; the second request might invalidate the first, causing a “Silent Logout.”
  3. Logical Inconsistencies: Concurrent writes to your secure token storage can lead to “Zombie” sessions where threads fight over outdated credentials.

🧩 System Design View: The Auth Loop

To understand the fix, we have to look at the entire flow from the client to the API Gateway:

  1. Client (Android): Interceptor adds the JWT to the header.
  2. API Gateway: Validates the JWT signature (Public Key).
  3. 401 Unauthorized: If expired, the Gateway rejects the call.
  4. Authenticator: The client catches the 401, triggers the Refresh API, and updates the Vault.

🏛️ Single-Flight Token Refresh

A Staff-level layer ensures Single-flight execution. Only one request is allowed to refresh the token; all other concurrent requests “pause” and wait.

The Tool: Kotlin Mutex

We use kotlinx.coroutines.sync.Mutex for coroutine-level mutual exclusion.

Staff Nuance: Inside OkHttp’s Authenticator, we use runBlocking to bridge the synchronous Java API. Since this runs on OkHttp’s internal thread pool, it won't starve the Main thread, but it ensures JWT token refresh Android logic remains atomic.

🛠️ Implementation: The Atomic Authenticator

Step 1: The Auth Interceptor (Proactive)

Ensures every outgoing request has the latest hardware-backed token.

Step 2: The Atomic Authenticator (Reactive)

Handles the 401 gracefully. We include a responseCount check to prevent refresh token race conditions and infinite loops.

⚠️ When This Breaks in Production

Even with a perfect OkHttp interceptor authentication strategy, edge cases exist:

  • Process Death: If the app is killed during a refresh, the Vault might be in an inconsistent state. Always verify token integrity on app launch.
  • Backend Idempotency: If the network cuts out after the backend rotates a token but before the client saves it, the next call will fail. Solution: Ensure the backend supports idempotent refresh (allowing the same refresh token to be used twice within a very narrow 500ms window).
  • Clock Drift: If the device clock is significantly off, your “isExpired” checks might fail. Always trust the server’s 401 over local timestamps.

🏁 Key Takeaways

  • Stateless auth removes server bottlenecks for 5M+ users.
  • Secure storage is non-negotiable; use the Keystore.
  • Concurrency bugs are the real enemy; use Mutex to enforce single-flight refreshes.
  • Fail-Closed: If refresh fails twice, log the user out to maintain security.

🙋‍♂️ Frequently Asked Questions (FAQs)

Is Mutex better than synchronized?

Yes. Mutex is non-blocking for coroutines and is the modern standard for thread-safe Kotlin development.

What happens if the Refresh Token is expired?

Our logic returns null, stopping the retry loop. At this point, broadcast a "Session Expired" event to navigate the user back to Login.

💬 Join the Discussion

  • How do you handle “Session Expired” UX across a complex app?
  • Have you ever seen a “Refresh Storm” take down a production server?
  • Does your backend support idempotent token rotation?

📘 Master Your Next Technical Interview

Since Java is the foundation of Android development, mastering DSA is essential. I highly recommend “Mastering Data Structures & Algorithms in Java”. It’s a focused roadmap covering 100+ coding challenges to help you ace your technical rounds.


Comments

Popular posts from this blog

No More _state + state: Simplifying ViewModels with Kotlin 2.3

Why You Should Stop Passing ViewModels Around Your Compose UI Tree 🚫

Is Jetpack Compose Making Your APK Fatter? (And How to Fix It)