<FlutterSolution/> flutter · dart · clean architecture

Flutter Interview Prep # 23 Designing the Networking Layer: Interceptors, REST vs GraphQL, and Real-Time Data

Designing the Networking Layer: Interceptors, REST vs GraphQL, and Real-Time Data

Move past "I use dio" to actual architectural decisions: token refresh races, connection lifecycles, and the BFF pattern.

01. Beyond the HTTP Client

If an interviewer asks how you handle networking in Flutter, answering "I use Dio" or "I use the http package" is a beginner's response. The senior question is not about the library itself; it is about how you design the layer AROUND the client.

Imagine a ride-hailing app. The user is on the map screen, which is actively polling for nearby drivers, checking current surge pricing, and fetching the user's active ride history. If the user's authentication token expires, you do not want three separate widgets independently attempting to refresh the auth token, failing, and throwing the user out to the login screen. You need a centralized networking layer that handles authentication, retries, logging, and cancellation invisibly, so your 50 UI screens only ever deal with clean data or fatal errors.

02. Dio Interceptors: Solving Hard UX Problems

The standard mechanism for building this shared layer in Flutter is the Interceptor pattern (most commonly via dio). An interceptor sits between your app code and the network, catching requests before they leave and responses before they reach the UI. But naming interceptors isn't enough; you must explain the specific problems they solve.

Auth Token Refresh-on-401 & The Race Condition

The classic token refresh flow works like this: a request fails with a 401 Unauthorized status. The interceptor pauses the error, triggers a call to your /refresh endpoint using a stored refresh token, and if successful, attaches the new access token and retries the original request. The UI never even knows the 401 happened.

But you'll hit this exact wall the first time your app loads a complex dashboard: The Race Condition. If 5 simultaneous API calls all return a 401 at the same time, a naive implementation will trigger 5 simultaneous token refresh calls. This invalidates tokens, spams the backend, and usually causes 4 of the 5 requests to fail permanently.

Token Refresh Race Condition
GET /drivers
GET /surge
GET /history
GET /profile
GET /wallet
401
Auth API
(/refresh)

Naive implementation: 5 requests fail -> 5 simultaneous refresh calls trigger.

The fix is conceptually simple but crucial for system design: a shared in-flight refresh future. When the first 401 hits, you assign the refresh task to a variable. When the other four 401s hit milliseconds later, they see the refresh is already in progress and simply await that existing future instead of starting their own.

Dart (Interceptor Fix)
Future<void>? _activeRefresh;

void onError(DioException err, ErrorInterceptorHandler handler) async {
  if (err.response?.statusCode == 401) {
    // 1. If no refresh is active, start one and store the Future
    if (_activeRefresh == null) {
      _activeRefresh = _performTokenRefresh();
    }
    
    try {
      // 2. ALL requests failing with 401 wait on this single Future lock
      await _activeRefresh;
      
      // 3. Retry original request with new token
      final retryResponse = await _retry(err.requestOptions);
      return handler.resolve(retryResponse);
    } catch (e) {
      return handler.next(err);
    } finally {
      // 4. Clean up the lock when done
      _activeRefresh = null;
    }
  }
  return handler.next(err);
}

Request/Response Logging & PII Redaction

Logging network calls is standard, but simply printing raw request/response bodies to a production log aggregator (like Datadog or Crashlytics) is a massive security risk. A production-grade logging interceptor must actively redact Personally Identifiable Information (PII). If a user updates their profile, their plaintext phone number, driver's license, or Bearer tokens should never end up in your logs. You must explicitly filter sensitive fields rather than logging everything indiscriminately by default.

Retry with Exponential Backoff

When a request fails due to a timeout or a 503 Service Unavailable, immediately retrying it on a fixed interval (e.g., every 1 second) is dangerous. If your ride-hailing backend is struggling under Friday night load, thousands of mobile clients aggressively retrying every second will cause a "retry storm," effectively DDoSing your own servers right when they are weakest. An interceptor implementing Exponential Backoff (retrying at 2s, 4s, 8s, 16s) gives the backend breathing room to recover.

Request Cancellation Tokens

Imagine a user is searching for a destination address. They type "123 Main St", an API call fires, but before it returns, they fix a typo and search "124 Main St". Without cancellation tokens, the first request completes wastefully in the background, and worse, if the network is flaky, the response for "123" might arrive after "124", overwriting the UI with stale, incorrect data. Passing a CancelToken tied to the UI component's lifecycle allows the networking layer to instantly abort in-flight requests that are no longer relevant.

03. REST vs GraphQL on Mobile Clients

Interviewers love the REST vs GraphQL debate, but as a mobile developer, you must evaluate it through the lens of mobile constraints, not generic backend theory.

GraphQL's core pitch is fixing over-fetching and under-fetching. A client specifies exactly what fields it needs. For a bandwidth-constrained mobile device on a spotty cellular connection, minimizing payload size by omitting unused data is a massive performance win.

But be explicit about the cost: GraphQL adds heavy client-side complexity. To use it effectively in Flutter (via packages like ferry or graphql_flutter), you must rely heavily on code generation for typed queries. More importantly, you must manage Cache Normalization. If Screen A fetches a Driver object with 3 fields, and Screen B fetches the same Driver with 5 fields, the GraphQL client needs a normalized local cache to merge these responses and ensure UI consistency without redundant network calls. This is significantly more complex than a standard REST setup.

REST + Typed Client (e.g. Retrofit)
Simple, predictable, and leverages standard HTTP caching and status codes. Low client-side boilerplate once codegen is setup.
✓ Best when: The API surface is stable, focused, and payload sizes aren't a critical bottleneck.
GraphQL (e.g. Ferry)
Eliminates over-fetching. Requires normalized caching to merge overlapping data fetched by different screens.
✓ Best when: MANY independently-evolving screens hit a shared backend with overlapping data needs.

04. Real-Time Data & WebSocket Lifecycles

When tracking a driver's live location on a map, standard HTTP requests aren't enough. You need real-time data transport. A senior candidate knows the differences between the three main approaches:

Protocol Direction Mobile Use Case
WebSocket Bidirectional High-frequency, low-latency updates (e.g., live driver GPS). Persistent TCP connection.
Server-Sent Events (SSE) Server-to-Client Simpler one-way streams (e.g., live price ticker) over plain HTTP.
Long-Polling Client pulls Lowest-common-denominator fallback when sockets are blocked by firewalls.

If you choose WebSockets (via web_socket_channel), you cannot just say "I open a connection." In a mobile environment, the connection WILL drop. The app goes to the background, the user steps into an elevator, or the phone hands off from Wi-Fi to cellular. A production WebSocket implementation must manage a resilient lifecycle.

Mobile Client
Connected (Ping/Pong Active) Connection Dropped (Tunnel) Reconnecting (Backoff: 2s, 4s...) Reconnected Successfully
Live Server

To survive mobile networks, your WebSocket layer must implement:

  • Reconnect Logic with Backoff: When the socket closes, you must attempt to reconnect automatically, using the same exponential backoff strategy as HTTP retries to avoid hammering the server.
  • Heartbeat / Ping-Pong: Mobile network proxies and NAT gateways often kill idle TCP connections silently. The socket might appear "open" to the OS, but data is no longer flowing. You must send periodic lightweight "ping" messages. If the server doesn't "pong" back within a timeout window, the client must manually terminate the dead connection and trigger the reconnect logic.

05. The Backend-for-Frontend (BFF) Pattern

System design interviews often feature this prompt: "Design a ride-summary screen that needs driver profile data, route map data, surge pricing data, and the user's wallet balance."

If the backend is composed of microservices, a naive mobile client will make 4 separate HTTP calls to 4 different services, wait for them all to return, and do the data aggregation (the JOIN) locally on the device. This is slow, drains the battery, and requires complex client-side error handling if 3 succeed but 1 fails.

Without BFF
N calls + Client-side Join
📱 Mobile App
Driver API
Route API
Price API
With BFF
1 call + Server-side Join
📱 Mobile App
⚙️ Mobile BFF Layer
Driver API
Route API
Price API

The architectural solution is a Backend-for-Frontend (BFF). A BFF is a dedicated backend layer shaped exactly to the needs of the mobile app. The mobile client makes exactly one call to the BFF. The BFF, running in the cloud with high-speed internal networking, orchestrates the 4 microservice calls, aggregates the data into exactly the JSON shape the UI needs, and returns it. Naming and explaining the BFF pattern is one of the highest-leverage answers you can give in a system design round.

06. Connecting the Patterns

Notice a recurring theme? In the Navigation post, we discussed how to handle a deep link that hits an auth guard: you capture the user's intent, route them to login, and then resume their intent once authenticated.

The token-refresh interceptor is the exact same architectural pattern applied to networking: capture the intent (the original failed request), handle the gate (the shared token refresh future lock), and resume (retry the request). Recognizing these structural similarities across different domains is what signals true senior-level engineering.


🧠 Interviewer's Follow-Up

Q: How do you avoid triggering 5 simultaneous token refreshes when a dashboard loads?
Pointer: Explain the shared in-flight Future lock. The first 401 triggers the refresh and stores the Future; subsequent 401s simply await that same Future before retrying.

Q: Why do WebSockets need a ping-pong heartbeat if TCP is already a persistent connection?
Pointer: Mobile network proxies and NAT gateways routinely drop idle TCP connections without notifying the client. The heartbeat detects silently-dead sockets.

Senior-Level Sample Answer

"For a ride-hailing app, I'd design the networking layer using Dio for REST calls and a dedicated WebSocket manager for live driver tracking. At the HTTP layer, I'd rely heavily on interceptors to centralize concerns: an auth interceptor that uses a shared future lock to prevent token-refresh race conditions, and a logging interceptor that explicitly redacts PII. For the real-time map data, I wouldn't just open a WebSocket; I’d wrap it in a manager class that handles automatic reconnection with exponential backoff and continuous ping-pong heartbeats to detect when the mobile radio silently drops the connection."

✅ Quick Self-Check

Without looking at the text above, explain why implementing Exponential Backoff on network retries is critical for the health of your backend servers.

Official Resources


Written by Flutter Solution · Covering the Flutter & Dart ecosystem from Mumbai, India · fluttersolution.com

Nachiketa Pandey
// written by

Nachiketa Pandey

Senior Flutter Developer · Cross-Platform & Fintech Applications

Building production Flutter apps at Kotak Neo — clean architecture, BLoC, agentic AI integrations, and fintech-grade security. Sharing everything I learn, one post at a time.

Flutter & Dart Clean Architecture BLoC Firebase FinTech
View Full Portfolio

Comments

Nachiketa
Meet Author// tap to open