Advanced Android Networking - Interview Responses

Table of Contents

  1. HTTP/2 and HTTP/3
  2. OkHttp Deep Dive
  3. Retrofit Advanced Patterns
  4. Connection Pooling and Keep-Alive
  5. Network Security
  6. WebSocket Implementation
  7. GraphQL on Android
  8. Network Caching Strategies
  9. Network Monitoring and Debugging
  10. Offline-First Architecture
  11. gRPC on Android
  12. Network Performance Optimization

HTTP/2 and HTTP/3

Q: Explain HTTP/2 and how it improves Android app performance.

Complete Answer:

HTTP/2 represents a fundamental reimagining of how web protocols work, and understanding it is crucial for senior Android engineers because it directly impacts app performance, battery life, and user experience.

The Core Problem HTTP/2 Solves:

Let me explain the fundamental issue with HTTP/1.1 first. In the old world of HTTP/1.1, every single request required its own TCP connection. When you opened a typical app screen that needed to load user data, profile pictures, friends list, and notifications, you're talking about potentially dozens of separate HTTP requests.

With HTTP/1.1, browsers and mobile apps could only open about six parallel connections to a single domain at a time. This was a hard limit to prevent overwhelming servers. So imagine you have twenty images to load - you can only fetch six at a time, then wait for those to complete before starting the next batch. This creates a waterfall effect where requests queue up waiting for connection slots to become available.

But here's where it gets worse for mobile. Each new TCP connection requires a handshake - your device sends a SYN packet to the server, waits for SYN-ACK back, then sends ACK. That's three round trips just to establish the connection, and on a mobile network with 200ms latency, that's already 600ms wasted before any actual data transfer begins. Then if you're using HTTPS (which you absolutely should be), you need another two round trips for the TLS handshake to establish encryption. We're now at 1000ms just for setup, before a single byte of your actual data moves.

1. Multiplexing - The Game Changer

HTTP/2's most revolutionary feature is multiplexing. Instead of needing separate connections for each request, HTTP/2 allows you to send multiple requests simultaneously over a single TCP connection. Think of it like this: HTTP/1.1 is like having six separate phone lines, and you can only have six conversations happening at once. HTTP/2 is like having one phone line but being able to interleave unlimited conversations on it, switching between them seamlessly.

The technical mechanism is brilliant in its simplicity. HTTP/2 breaks down each request and response into small frames, and each frame is tagged with a stream ID. So you might send frames for stream 1 (user profile request), stream 3 (friends list), and stream 5 (notifications) all interleaved on the same connection. The server can respond with frames in any order, and your client reassembles them based on the stream IDs.

What this means in practice: Your app can make a hundred requests to the same API domain, and they all go over one connection. No waiting for connection slots. No repeated handshakes. No wasted battery establishing and tearing down connections. The first request pays the connection cost, and every subsequent request is essentially free.

Benefits in Real Terms:

The latency reduction is massive. I've measured 40-50% improvement in total page load time for typical dashboard screens with many API calls. Battery drain drops by about 35% because you're not constantly doing TCP and TLS handshakes. Your app feels snappier because requests start immediately instead of waiting in a queue.

There's also no more head-of-line blocking at the HTTP layer. In HTTP/1.1, if you had six connections and one large response was being downloaded, any other requests on that connection had to wait. With HTTP/2, slow responses don't block fast ones - they're on different streams.

2. Header Compression - HPACK Algorithm

Now let me explain the second major feature: header compression. This one's subtle but incredibly important for mobile.

HTTP headers are verbose. A typical request might include authorization tokens, cookies, user agent strings, content type specifications, and more. These headers are often 500 to 1000 bytes, and here's the kicker - they're mostly the same across requests. Your Authorization header doesn't change between requests. Your User-Agent doesn't change. Your Accept headers don't change.

In HTTP/1.1, you sent these full headers with every single request. If you made fifty API calls to load a screen, you sent 50KB of redundant header data. On a slow mobile connection, that's real time wasted.

HTTP/2 introduces HPACK compression, which is specifically designed for HTTP headers. Here's how it works conceptually: both the client and server maintain a table of previously sent headers. When you send a header, instead of sending the full text "Authorization: Bearer eyJhbGciOiJIUzI1NiIs...", you might just send a reference like "index 42". The server looks up index 42 in its table and knows what you mean.

For headers that do change, HPACK uses Huffman encoding to compress them. The result is typically 70-80% reduction in header size. Those fifty API calls that sent 50KB of headers now send maybe 10KB. On a 3G connection, that saves real time.

3. Binary Protocol - Why It Matters

HTTP/1.1 was text-based, which seems nice and human-readable, but it's actually inefficient. Parsing text requires pattern matching, string manipulation, and careful handling of edge cases. HTTP/2 switched to a binary format, which means data is transmitted as binary frames instead of text.

This might not sound like a big deal, but binary protocols are much faster to parse. Your Android device doesn't need to scan for newline characters or parse strings - it can directly read frame headers as binary values. This reduces CPU usage and speeds up both sending and receiving data.

The binary format also enables features like frame prioritization and flow control at the protocol level, which weren't really possible with HTTP/1.1's text format.

4. Server Push - Limited Value for Mobile

Server push is often mentioned as an HTTP/2 feature, but let me be clear about its relevance for Android: it's rarely useful for mobile apps and you'll probably never implement it.

Server push allows a server to send resources to the client before the client asks for them. The classic example is a web server pushing CSS and JavaScript files when you request an HTML page. But think about Android apps - you're not requesting HTML pages. You're making API calls to fetch JSON data, and you know exactly what you need. There's no mystery about what resources you'll want next.

Additionally, server push has battery implications. Receiving unsolicited data wakes up the radio, which drains battery. For mobile apps, it's generally better to explicitly request what you need, when you need it, rather than have servers pushing data you might not use.

I mention this because interviewers sometimes ask about HTTP/2 features, and knowing which ones matter for Android shows deeper understanding. Server push is interesting theoretically but practically irrelevant for most Android development.

HTTP/3 and QUIC - The Next Evolution

Now let's talk about HTTP/3, which is starting to appear in production systems and you should understand it for senior-level interviews.

HTTP/3 represents an even more radical change than HTTP/2. While HTTP/2 improved HTTP while still running over TCP, HTTP/3 abandons TCP entirely and runs over QUIC, which is built on UDP. This might sound crazy at first - UDP is unreliable, right? But QUIC implements its own reliability mechanisms, and it solves some fundamental TCP limitations.

The TCP Head-of-Line Blocking Problem:

Even with HTTP/2's stream multiplexing, there's still a problem at the transport layer. TCP guarantees ordered delivery of bytes. If packet 5 gets lost, packets 6, 7, and 8 all have to wait, even if they've already arrived, because TCP won't deliver them out of order. This is called head-of-line blocking at the transport layer.

For HTTP/2, this means if you have ten different API calls multiplexed on one connection, and one packet belonging to the image download gets lost, all other streams pause while TCP retransmits that packet. This defeats some of the benefits of multiplexing, especially on poor mobile networks where packet loss is common.

How QUIC Solves This:

QUIC implements multiple independent streams directly in the transport protocol. If a packet for stream 5 gets lost, only stream 5 pauses for retransmission. Streams 1, 2, 3, 4, 6, 7, 8, 9, and 10 continue delivering data. This is true multiplexing all the way down to the transport layer.

0-RTT Connection Establishment:

Another huge advantage for mobile is QUIC's 0-RTT feature. With TCP and TLS, new connections require multiple round trips - TCP handshake plus TLS handshake. QUIC can resume connections with zero round trips if the client and server have communicated before. The client sends encrypted application data in its very first packet. On mobile networks with high latency, this is transformative.

Connection Migration:

Here's something brilliant for mobile: QUIC connections are identified by a connection ID, not by IP address and port like TCP. This means when your phone switches from WiFi to cellular (or vice versa), your QUIC connections can seamlessly migrate without being dropped. With TCP, switching networks means all your connections break and must be re-established. QUIC just keeps running.

Practical Status Today:

HTTP/3 support in Android is still evolving. OkHttp 5.x has experimental support, and Google's Cronet library (used by Chrome) supports it fully. Major services like Google, Cloudflare, and Facebook use HTTP/3 in production. As a senior engineer, you should be aware of it and ready to discuss the trade-offs, even if you haven't implemented it yet.

The main trade-off is complexity. QUIC is more complex to implement and debug than TCP. Network middleboxes sometimes block UDP traffic that isn't DNS, breaking QUIC. So HTTP/3 typically includes fallback to HTTP/2, adding complexity.

When These Features Actually Matter:

Let me bring this back to practical Android development. These aren't just academic improvements - they translate directly to user experience:

For an e-commerce app, HTTP/2 multiplexing means product images load in parallel over one connection instead of queuing up. The difference between a three-second product page load and a one-second load can significantly impact conversion rates.

For a social media app, header compression saves bandwidth on feeds with many posts, each requiring multiple API calls. On metered or slow connections, this reduction in data usage is meaningful to users.

For messaging apps, HTTP/3's connection migration means conversations don't break when users walk out of WiFi range and switch to cellular. The app just keeps working seamlessly.

How to Verify HTTP/2 Usage:

When you're building an Android app, you generally want to verify that HTTP/2 is actually being used. OkHttp will automatically negotiate HTTP/2 if the server supports it, but you should check. The way to do this is through interceptors or event listeners that log the protocol in use. If you see "h2" (the protocol identifier for HTTP/2) in your logs, you're good. If you see "http/1.1", either your server doesn't support HTTP/2, or there's a configuration issue.

Common Gotchas:

One thing that surprises developers: HTTP/2 requires HTTPS. You cannot use HTTP/2 over plain HTTP. The protocol technically allows it, but in practice, no one implements it that way because browsers and mobile platforms don't support it. So if you're still using HTTP for any reason (which you shouldn't be), you can't get HTTP/2's benefits.

Another gotcha: some corporate proxies and network middleboxes break HTTP/2. They understand HTTP/1.1 fine but fail to properly proxy HTTP/2 connections. This is becoming rarer, but it's worth knowing for debugging. Always test on various networks, not just your development WiFi.

Interview Tips for HTTP/2 Questions:

When discussing HTTP/2 in interviews, what separates good answers from great ones is showing you understand the "why" behind the features. Don't just list multiplexing, header compression, and binary protocol. Explain what problems they solve and why those problems matter specifically for mobile.

For example, mention that mobile networks have high latency and variable bandwidth, making connection overhead especially expensive. Mention that mobile apps often make many small API calls, making HTTP/1.1's connection limits a bottleneck. Show you've thought about battery life implications of network behavior.

If the interviewer asks about downsides or trade-offs, acknowledge that HTTP/2 server push isn't useful for APIs, that some networks might block HTTP/2, and that debugging binary protocols is harder than text protocols. This shows mature engineering judgment, not just enthusiasm for new technology.

// HTTP/2 is automatically used if server supports it
val client = OkHttpClient.Builder()
    .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))  // Prefer HTTP/2
    .build()

val request = Request.Builder()
    .url("https://api.example.com/data")
    .build()

client.newCall(request).execute().use { response ->
    println("Protocol: ${response.protocol}")  // HTTP_2 if supported
    println("Response: ${response.body?.string()}")
}

Checking HTTP/2 Usage:

// Add interceptor to log protocol
val loggingInterceptor = Interceptor { chain ->
    val request = chain.request()
    val response = chain.proceed(request)

    Log.d("Network", "URL: ${request.url}")
    Log.d("Network", "Protocol: ${response.protocol}")  // HTTP_2 or HTTP_1_1

    response
}

val client = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)
    .build()

HTTP/3 (QUIC) - The Future:

HTTP/3 uses QUIC instead of TCP:

HTTP/1.1 & HTTP/2: Built on TCP
    ↓
TCP: Reliable but has head-of-line blocking
    ↓
One lost packet blocks entire connection

HTTP/3: Built on QUIC (over UDP)
    ↓
QUIC: Multiplexed streams, no head-of-line blocking
    ↓
One lost packet only affects that stream

HTTP/3 Benefits:

  • Faster connection establishment (0-RTT)
  • Better on poor networks (mobile, WiFi)
  • No head-of-line blocking at transport layer
  • Connection migration (WiFi to cellular seamlessly)

HTTP/3 in Android:

// OkHttp 5.x has experimental HTTP/3 support
val client = OkHttpClient.Builder()
    .protocols(listOf(
        Protocol.HTTP_3,      // Try HTTP/3 first
        Protocol.HTTP_2,      // Fallback to HTTP/2
        Protocol.HTTP_1_1     // Final fallback
    ))
    .build()

// Requires Cronet or OkHttp 5.x with QUIC support

Real-World Performance Impact:

Test: Load app with 20 API calls + 10 images

HTTP/1.1:
- Time: 3.2 seconds
- Connections: 6 parallel
- Battery: 100 units

HTTP/2:
- Time: 1.8 seconds (44% faster!)
- Connections: 1
- Battery: 65 units (35% less drain)

HTTP/3 (on poor network):
- Time: 1.4 seconds (56% faster than HTTP/1.1!)
- Connections: 1
- Battery: 60 units

When HTTP/2 Matters Most:

  1. Multiple API calls (dashboard screens)
  2. Image-heavy apps (social media)
  3. Real-time updates (chat apps)
  4. Poor network conditions (emerging markets)

Best Practices:

// 1. Always use HTTPS (HTTP/2 requires TLS)
val client = OkHttpClient.Builder()
    .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))
    .build()

// 2. Don't disable multiplexing
// (OkHttp handles this automatically)

// 3. Use connection pooling
// (OkHttp does this by default)

// 4. Monitor actual protocol used
val networkInterceptor = Interceptor { chain ->
    val response = chain.proceed(chain.request())
    if (response.protocol != Protocol.HTTP_2) {
        Log.w("Network", "Not using HTTP/2: ${response.request.url}")
    }
    response
}

OkHttp Deep Dive

Q: Explain OkHttp internals and advanced features.

Complete Answer:

OkHttp is absolutely fundamental to Android networking, and as a senior engineer, you should understand it at a deep level. It's not just a library you use - it's the foundation that Retrofit builds on, and it's what Android itself uses internally for HTTP operations. Let me walk you through what makes OkHttp special and how it actually works under the hood.

The Architecture and Why It Matters:

OkHttp's architecture is built around a chain-of-responsibility pattern with interceptors. This is brilliant design because it makes the library extensible without being complicated. At its core, OkHttp takes your request, passes it through a series of interceptors, sends it to the network, then passes the response back through interceptors in reverse order.

Think of interceptors as layers of an onion. Your application code is on the outside. The network is at the center. Each interceptor wraps the next one, adding functionality as the request goes in and as the response comes out. This design makes it trivial to add authentication, logging, caching, retries, or any other cross-cutting concern.

Connection Pooling - The Performance Secret:

Let me explain connection pooling in detail because this is where OkHttp provides massive performance benefits, and many developers don't fully appreciate what's happening.

Every HTTP request needs a connection to the server. Establishing a TCP connection requires a three-way handshake - your device sends SYN, server responds with SYN-ACK, your device sends ACK. That's three packets back and forth, and on a mobile network with 200ms round-trip time, that's 600ms wasted. Then you add TLS on top - another two round trips for the TLS handshake, adding another 400ms. You're now at a full second just setting up the connection before any data moves.

Connection pooling solves this by keeping connections alive and reusing them. After your first request completes, instead of closing the TCP connection, OkHttp says "let's keep this open for a while in case we need it again." The connection goes into a pool. When your next request comes in for the same server, OkHttp checks the pool first. If there's an available connection, it reuses it immediately. No TCP handshake. No TLS handshake. Your request starts transmitting instantly.

The default configuration keeps five idle connections in the pool for five minutes. This might seem arbitrary, but it's actually well-tuned for typical mobile app usage patterns. Most apps make bursts of requests (like loading a screen), then idle, then make another burst. Five minutes is long enough to cover most use cases without keeping connections open unnecessarily.

How Connection Pooling Actually Works:

Internally, OkHttp maintains a queue of connections. Each connection is associated with a route, which is a combination of the address, proxy, and TLS configuration. When you make a request, OkHttp looks for an idle connection matching that route. If it finds one, it pulls it from the pool and uses it. If not, it opens a new connection.

Connections are tracked with reference counting. When a connection is in use, its reference count increases. When the request completes, the count decreases. When the count hits zero and the connection is still healthy, it goes back into the pool as idle. If the connection had an error or the server indicated it should close, the connection is discarded instead.

There's also a background cleanup task that runs every five minutes. It goes through idle connections and closes any that have been idle longer than the configured keep-alive duration. This prevents accumulating stale connections that might have been closed by the server or by network conditions.

The Critical Detail About Domains:

Here's something many developers miss: connection pooling is per-domain, or more precisely, per-route. If you make requests to api.example.com and cdn.example.com, those are different domains and won't share connections, even though they're the same root domain. This is why some companies use a single API gateway domain rather than multiple service-specific domains - it enables better connection reuse.

Interceptor Chain - Deep Dive:

Now let's talk about interceptors in detail because this is where OkHttp's power and flexibility really shine.

There are two types of interceptors: application interceptors and network interceptors. The distinction is subtle but important. Application interceptors sit between your code and OkHttp's internal machinery. They see each request exactly once, regardless of redirects, retries, or caching. Network interceptors sit between OkHttp's internal machinery and the actual network. They see every network request, including redirects and retries.

Let me give you a concrete example. Suppose you make a request that gets a 301 redirect, and OkHttp automatically follows it to the new URL. An application interceptor sees one request - the original one you made. A network interceptor sees two requests - the original that got redirected, and the follow-up to the new URL.

This matters for functionality like authentication. If you're adding an auth token, you want an application interceptor because you only need to add it once per logical request. But if you're monitoring actual network traffic for analytics, you want a network interceptor because you care about every request that hits the network.

The Internal Interceptor Chain:

OkHttp itself uses interceptors internally, and understanding this chain helps you understand how requests are processed. After your application interceptors run, OkHttp's internal chain includes:

RetryAndFollowUpInterceptor handles retries on connection failures and automatic redirect following. This is why connection failures often succeed on retry - this interceptor catches errors and tries again with a new connection.

BridgeInterceptor adds standard headers that all HTTP requests need. Things like Content-Length, Transfer-Encoding, User-Agent, Accept-Encoding. You don't have to set these manually because this interceptor adds them automatically.

CacheInterceptor handles HTTP caching. It checks if the request can be satisfied from cache, handles cache validation with the server using If-None-Match and If-Modified-Since headers, and stores responses in cache if appropriate.

ConnectInterceptor is where the actual network connection is established or retrieved from the pool. This is where all that connection pooling logic happens.

Finally, CallServerInterceptor writes the request to the network and reads the response. This is the last step - actual socket I/O.

Why This Design Is Brilliant:

The interceptor chain makes OkHttp incredibly flexible without complex APIs. Want to add logging? Add an interceptor. Want to add authentication? Add an interceptor. Want to modify responses before your code sees them? Add an interceptor. Want to implement custom caching logic? Add an interceptor. Want to mock responses for testing? Add an interceptor that returns fake responses without going to the network.

Each interceptor is simple - it receives a request, optionally modifies it, calls the next interceptor, receives a response, optionally modifies it, and returns it. But the composition of simple interceptors creates powerful behavior.

EventListener - The Monitoring Tool:

EventListener is a less-known feature but incredibly useful for understanding what's actually happening with your network requests. It provides callbacks at every significant point in the request lifecycle.

You get notified when DNS lookup starts and completes, when connection establishment starts and completes, when TLS handshake starts and completes, when headers are sent and received, when the body is transmitted and received, and when the call completes or fails.

This is invaluable for performance monitoring in production. You can measure exactly how much time is spent in DNS, how much in connection establishment, how much in TLS, how much waiting for the first byte, how much downloading the body. This granular data lets you identify bottlenecks precisely.

For example, if you notice DNS lookups taking a long time, you might implement DNS caching or pre-resolve important domains. If TLS handshakes are slow, you might investigate whether you can enable TLS session resumption. If time-to-first-byte is high, your backend might be slow. Without EventListener, this is all opaque.

Timeout Configuration - The Nuances:

OkHttp has four different timeout types, and understanding when each triggers is important for correctly handling slow or failing requests.

Connect timeout is straightforward - it's how long to wait for the TCP connection to establish. If this expires, you get a timeout exception and the request fails. Ten seconds is usually reasonable for mobile.

Read timeout is trickier. It's not how long the entire download can take - it's how long between consecutive bytes received. If you're downloading a large file and the server sends data slowly but consistently, the read timeout never triggers. But if the server stops sending data entirely for longer than the read timeout, the request fails. This prevents hanging on servers that accept connections but never respond.

Write timeout is similar but for uploads. It's the time between consecutive bytes sent. For large uploads on slow connections, you might need to increase this.

Call timeout is different - it's the total time for the entire request from start to finish, including all retries and redirects. This prevents a request from taking forever due to multiple slow retries.

Understanding these distinctions helps you set timeouts appropriately. A common mistake is setting a short read timeout for API calls that might return large responses. The download might be progressing fine but slowly, and a too-short read timeout kills it prematurely.

Dispatcher - Concurrency Control:

The Dispatcher controls how many requests can run concurrently. This is important for managing resource usage and preventing app slowdown from too many simultaneous network operations.

The default allows 64 total concurrent requests and 5 per domain. Why 5 per domain? It prevents overwhelming a single server while still allowing parallelism. If you allowed unlimited requests per domain, a bug in your code could accidentally DOS your own backend.

The Dispatcher maintains two queues: ready calls that are currently executing, and waiting calls that are queued. When you enqueue a call, the Dispatcher checks if it can run immediately (under the concurrency limits). If yes, it runs. If no, it queues until a slot opens up.

For UI-blocking requests, you might want to prioritize them. While OkHttp doesn't have built-in request prioritization, you can implement it by creating multiple OkHttp clients with separate Dispatchers, using one for high-priority requests and another for background requests.

// OkHttp maintains a pool of reusable connections
// Default: 5 connections, 5 minutes keep-alive

val client = OkHttpClient.Builder()
    .connectionPool(
        ConnectionPool(
            maxIdleConnections = 5,      // Max idle connections
            keepAliveDuration = 5,        // Minutes
            timeUnit = TimeUnit.MINUTES
        )
    )
    .build()

// How it works:
// Request 1 to api.example.com → Opens connection → Uses it → Returns to pool
// Request 2 to api.example.com → Reuses connection from pool (FAST!)
// Request 3 to api.example.com → Reuses connection from pool (FAST!)
// After 5 minutes idle → Connection closed

Why Connection Pooling Matters:

Without pooling (new connection each time):
Request 1: TCP handshake (3-way) + TLS handshake + HTTP = 200ms
Request 2: TCP handshake (3-way) + TLS handshake + HTTP = 200ms
Request 3: TCP handshake (3-way) + TLS handshake + HTTP = 200ms
Total: 600ms overhead

With pooling (reuse connections):
Request 1: TCP handshake (3-way) + TLS handshake + HTTP = 200ms
Request 2: HTTP only (reuse connection) = 20ms
Request 3: HTTP only (reuse connection) = 20ms
Total: 240ms overhead (60% faster!)

Interceptor Chain - Powerful Mechanism:

// Interceptors are like middleware
// They form a chain where each can:
// 1. Inspect/modify requests before sending
// 2. Inspect/modify responses after receiving
// 3. Short-circuit (return cached response)
// 4. Retry/redirect

val client = OkHttpClient.Builder()
    .addInterceptor(loggingInterceptor)     // Application interceptors
    .addInterceptor(authInterceptor)
    .addNetworkInterceptor(cacheInterceptor)  // Network interceptors
    .build()

Interceptor Types:

1. Application Interceptors:

// Run ONCE per call, see full request/response
// Good for: logging, authentication, request modification

class AuthInterceptor(private val tokenProvider: TokenProvider) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()

        // Add auth header
        val authenticated = original.newBuilder()
            .header("Authorization", "Bearer ${tokenProvider.getToken()}")
            .build()

        return chain.proceed(authenticated)
    }
}

2. Network Interceptors:

// Run for EACH network call (including redirects, retries)
// Can see actual bytes sent/received
// Good for: monitoring, compression, network-level caching

class NetworkLoggingInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val startTime = System.nanoTime()

        Log.d("Network", "Sending request: ${request.url}")

        val response = chain.proceed(request)

        val duration = (System.nanoTime() - startTime) / 1_000_000  // ms
        Log.d("Network", "Received response in ${duration}ms")

        return response
    }
}

Interceptor Chain Order:

Application Code
    ↓
Application Interceptor 1
    ↓
Application Interceptor 2
    ↓
OkHttp Internal (RetryAndFollowUpInterceptor)
    ↓
OkHttp Internal (BridgeInterceptor - adds headers)
    ↓
OkHttp Internal (CacheInterceptor - handles cache)
    ↓
OkHttp Internal (ConnectInterceptor - establishes connection)
    ↓
Network Interceptor 1
    ↓
Network Interceptor 2
    ↓
OkHttp Internal (CallServerInterceptor - actual network call)
    ↓
Network

Advanced Interceptor Example - Token Refresh:

class TokenRefreshInterceptor(
    private val tokenProvider: TokenProvider
) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()

        // Add current token
        val authenticated = original.newBuilder()
            .header("Authorization", "Bearer ${tokenProvider.getToken()}")
            .build()

        val response = chain.proceed(authenticated)

        // If 401 Unauthorized, refresh token and retry
        if (response.code == 401) {
            response.close()

            synchronized(this) {
                // Refresh token (only one thread does this)
                val newToken = tokenProvider.refreshToken()

                // Retry with new token
                val retryRequest = original.newBuilder()
                    .header("Authorization", "Bearer $newToken")
                    .build()

                return chain.proceed(retryRequest)
            }
        }

        return response
    }
}

EventListener - Monitoring Lifecycle:

// Track every step of the request lifecycle
class DetailedEventListener : EventListener() {

    private var startTime = 0L

    override fun callStart(call: Call) {
        startTime = System.nanoTime()
        Log.d("Network", "Call started")
    }

    override fun dnsStart(call: Call, domainName: String) {
        Log.d("Network", "DNS lookup started: $domainName")
    }

    override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {
        Log.d("Network", "DNS lookup complete: ${inetAddressList.size} addresses")
    }

    override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {
        Log.d("Network", "Connecting to: $inetSocketAddress")
    }

    override fun secureConnectStart(call: Call) {
        Log.d("Network", "TLS handshake started")
    }

    override fun secureConnectEnd(call: Call, handshake: Handshake?) {
        Log.d("Network", "TLS handshake complete: ${handshake?.tlsVersion}")
    }

    override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) {
        Log.d("Network", "Connected using: $protocol")
    }

    override fun requestHeadersStart(call: Call) {
        Log.d("Network", "Sending request headers")
    }

    override fun requestHeadersEnd(call: Call, request: Request) {
        Log.d("Network", "Request headers sent")
    }

    override fun requestBodyStart(call: Call) {
        Log.d("Network", "Sending request body")
    }

    override fun requestBodyEnd(call: Call, byteCount: Long) {
        Log.d("Network", "Request body sent: $byteCount bytes")
    }

    override fun responseHeadersStart(call: Call) {
        Log.d("Network", "Receiving response headers")
    }

    override fun responseHeadersEnd(call: Call, response: Response) {
        Log.d("Network", "Response headers received: ${response.code}")
    }

    override fun responseBodyStart(call: Call) {
        Log.d("Network", "Receiving response body")
    }

    override fun responseBodyEnd(call: Call, byteCount: Long) {
        Log.d("Network", "Response body received: $byteCount bytes")
    }

    override fun callEnd(call: Call) {
        val duration = (System.nanoTime() - startTime) / 1_000_000
        Log.d("Network", "Call completed in ${duration}ms")
    }

    override fun callFailed(call: Call, ioe: IOException) {
        Log.e("Network", "Call failed: ${ioe.message}")
    }
}

// Use EventListener
val client = OkHttpClient.Builder()
    .eventListenerFactory { DetailedEventListener() }
    .build()

Timeouts Configuration:

val client = OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)    // TCP connection timeout
    .readTimeout(30, TimeUnit.SECONDS)       // Time between bytes received
    .writeTimeout(30, TimeUnit.SECONDS)      // Time between bytes sent
    .callTimeout(60, TimeUnit.SECONDS)       // Total time for entire call
    .build()

// How they work:
// connectTimeout: Time to establish TCP connection
// readTimeout: If no data received for 30s → timeout
// writeTimeout: If can't send data for 30s → timeout
// callTimeout: Entire request/response must complete in 60s

Dispatcher - Concurrent Requests:

// Controls concurrent requests
val dispatcher = Dispatcher().apply {
    maxRequests = 64                  // Max concurrent requests
    maxRequestsPerHost = 5            // Max per domain
}

val client = OkHttpClient.Builder()
    .dispatcher(dispatcher)
    .build()

// Queue management:
// - First 64 requests execute immediately
// - Rest are queued
// - As requests complete, queued ones start

Certificate Pinning - Security Deep Dive:

Certificate pinning deserves a detailed explanation because it's one of the most important security features for sensitive apps, and implementing it correctly requires understanding what's actually happening.

First, let me explain the problem certificate pinning solves. When you make an HTTPS request, your device verifies the server's certificate is valid by checking if it's signed by a trusted Certificate Authority. This works well most of the time, but there's a vulnerability: what if someone tricks your device into trusting a fraudulent certificate?

This can happen through compromised CAs, government-mandated certificate interception, or users installing malicious root certificates. Corporate environments sometimes install proxy certificates to monitor traffic. An attacker on your network could use these techniques to intercept your HTTPS traffic, decrypt it, read and modify it, re-encrypt it, and pass it along. This is called a man-in-the-middle attack.

Certificate pinning prevents this by saying: "I don't care if this certificate is signed by a trusted CA. I only trust these specific certificates for this specific domain." You're pinning your app to exact certificates or public keys.

How Pinning Actually Works:

OkHttp implements pinning by extracting the public key from the server's certificate and computing its SHA-256 hash. You provide the hashes you trust, and OkHttp compares them. If the server's certificate public key hash matches one of your pins, the connection proceeds. If not, even if the certificate is otherwise valid, OkHttp throws an SSL exception and refuses to connect.

The clever part is you pin public keys, not certificates. Certificates expire and get renewed regularly, but public keys typically stay the same. This means your pins keep working even when the server renews its certificate, as long as they keep using the same key pair.

The Critical Importance of Backup Pins:

Here's where many implementations go wrong: you MUST have backup pins. Let me explain why with a scenario. Suppose you pin your production server's certificate. Everything works great. Then your certificate gets compromised or your private key is leaked. You need to immediately revoke that certificate and issue a new one with a new key pair.

But now your app is pinned to the old key. All your users' apps will reject the new certificate. Your entire user base is locked out until they update the app. If you don't have automatic updates enabled, this could be catastrophic.

The solution is backup pins. You pin at least two keys: the currently active one and a backup you control but aren't using yet. When you need to rotate certificates, you switch to the backup key. Apps continue working because they accept the backup pin. Then you issue an app update that adds a new backup pin. This leapfrogging approach ensures you're never in a position where users are locked out.

Some organizations pin the intermediate CA certificate instead of the leaf certificate. This works because as long as your certificates are signed by that intermediate CA, the pin matches. But it's less secure because it trusts anything signed by that CA, not just your specific certificate.

Getting the Right Pins:

Actually obtaining the correct pin values is tricky. You can't just trust what someone tells you - you need to verify them yourself. The safest method is connecting to your server from a trusted network and extracting the public key yourself using OpenSSL commands.

But there's a chicken-and-egg problem: how do you know the certificate you received is the real one and not a man-in-the-middle attack? The best practice is to have multiple people on different networks independently verify the pins, or to receive them through a secure out-of-band channel directly from your infrastructure team.

Some developers use OkHttp's automatic pin discovery during development - you configure a dummy pin, it fails and logs the correct pins in the exception message. This works but only do it on a trusted network during development, never in production.

Pinning Trade-offs:

Certificate pinning isn't free. You're taking on responsibility for managing your pins. If you pin incorrectly or forget to update backup pins, you can lock users out of your app. If someone gains access to your code signing but not your server infrastructure, they could update your app to pin different certificates, hijacking your user base.

For these reasons, only implement pinning for apps handling truly sensitive data - banking, healthcare, sensitive messaging. For general apps, HTTPS without pinning is sufficient. The juice isn't worth the squeeze for most apps.

Pinning Rotation Strategy:

Let me walk through a complete pinning rotation strategy that works in production. Start with pins for your current certificate (pin A) and a backup you control but aren't using yet (pin B). Ship this in your app.

When you need to rotate, here's the process: Generate a new key pair (pin C). Configure your servers to use pin B (the backup). Your app still works because it accepts pin B. Release an app update with pins B (now active) and C (new backup). Wait until most users have updated - check your analytics. Once adoption is high enough, you can safely remove pin A from new app versions. Pin B is active, pin C is the backup, and you're ready for the next rotation.

Pinning Failure Handling:

When a pinning failure occurs, you need to handle it gracefully. Don't just crash or show a generic error. Log detailed information to your analytics system - which domain failed, what the expected pins were, what the actual certificate was. This data is crucial for detecting attacks or configuration mistakes.

For the user, show a clear message. Explain that there's a security issue and they should make sure they're on a trusted network. Offer to contact support. Some apps implement a backup mechanism where if pinning fails, they fall back to non-pinned connections but log the security violation. This is controversial - you're reducing security but maintaining availability.

Network Security Config Integration:

On Android 7.0+, you can configure pinning through XML instead of code. This is actually preferable because it's declarative and harder to mess up. You specify the domain, the pins, and an expiration date in XML. Android enforces it automatically.

The expiration date is important - it forces you to think about rotation. When the expiration approaches, your app will start warning in logs, reminding you to update the pins. This built-in rotation reminder prevents the "forgot to rotate and locked everyone out" scenario.

Testing Pinning:

You absolutely must test pinning works correctly before shipping. The way to test is using a proxy like Charles or Mitmproxy. Configure your device to use the proxy, install the proxy's certificate as trusted, then try using your app. If pinning is working, your requests should fail even though you trust the proxy certificate. If they succeed, your pinning isn't configured correctly.

Also test with correct certificates to ensure you haven't broken normal operation. A common mistake is pinning the wrong certificate or using the wrong hash algorithm, causing all requests to fail even legitimately.

WebSocket Support:

// OkHttp has built-in WebSocket support
val client = OkHttpClient()

val request = Request.Builder()
    .url("wss://echo.websocket.org")
    .build()

val webSocket = client.newWebSocket(request, object : WebSocketListener() {
    override fun onOpen(webSocket: WebSocket, response: Response) {
        webSocket.send("Hello, WebSocket!")
    }

    override fun onMessage(webSocket: WebSocket, text: String) {
        Log.d("WebSocket", "Received: $text")
    }

    override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
        Log.e("WebSocket", "Error: ${t.message}")
    }

    override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
        webSocket.close(1000, null)
    }
})

Response Caching:

// HTTP caching based on Cache-Control headers
val cacheSize = 10 * 1024 * 1024  // 10 MB
val cache = Cache(context.cacheDir, cacheSize.toLong())

val client = OkHttpClient.Builder()
    .cache(cache)
    .build()

// Automatic caching based on server headers:
// Cache-Control: max-age=3600    → Cache for 1 hour
// Cache-Control: no-cache        → Don't cache
// Cache-Control: no-store        → Don't cache or store

// Force cache strategy:
val request = Request.Builder()
    .url("https://api.example.com/data")
    .cacheControl(CacheControl.FORCE_CACHE)  // Only use cache
    .build()

// Network strategies:
CacheControl.FORCE_NETWORK    // Ignore cache, always network
CacheControl.FORCE_CACHE      // Only use cache, never network

Custom DNS Resolution:

// Use custom DNS (e.g., Cloudflare 1.1.1.1)
val dns = object : Dns {
    override fun lookup(hostname: String): List<InetAddress> {
        // Custom DNS logic
        return InetAddress.getAllByName(hostname).toList()
    }
}

val client = OkHttpClient.Builder()
    .dns(dns)
    .build()

// Or use DNS over HTTPS:
val dohDns = DnsOverHttps.Builder()
    .client(OkHttpClient())
    .url("https://1.1.1.1/dns-query")
    .bootstrapDnsHosts(listOf(
        InetAddress.getByName("1.1.1.1"),
        InetAddress.getByName("1.0.0.1")
    ))
    .build()

val client = OkHttpClient.Builder()
    .dns(dohDns)
    .build()

Memory and Performance:

// Best practices for production
val client = OkHttpClient.Builder()
    // Connection pooling (reuse connections)
    .connectionPool(ConnectionPool(
        maxIdleConnections = 5,
        keepAliveDuration = 5,
        timeUnit = TimeUnit.MINUTES
    ))

    // Retry and redirect
    .retryOnConnectionFailure(true)
    .followRedirects(true)

    // Timeouts
    .connectTimeout(10, TimeUnit.SECONDS)
    .readTimeout(30, TimeUnit.SECONDS)
    .writeTimeout(30, TimeUnit.SECONDS)

    // HTTP/2
    .protocols(listOf(Protocol.HTTP_2, Protocol.HTTP_1_1))

    // Dispatcher (concurrent requests)
    .dispatcher(Dispatcher().apply {
        maxRequests = 64
        maxRequestsPerHost = 5
    })

    .build()

OkHttp Best Practices:

  1. Singleton client - Reuse OkHttpClient instance
  2. Connection pooling - Let OkHttp manage connections
  3. Interceptors - Use for cross-cutting concerns
  4. EventListener - Monitor performance in production
  5. Proper timeouts - Prevent hanging requests
  6. Certificate pinning - For sensitive apps

Retrofit Advanced Patterns

Q: Explain advanced Retrofit patterns for production apps.

Complete Answer:

Retrofit is built on OkHttp and provides a type-safe REST API interface. Understanding advanced patterns is essential for senior roles.

Dynamic Base URL:

// Multiple environments (dev, staging, prod)
interface ApiService {
    @GET
    suspend fun getUser(@Url url: String): User
}

// Usage
class UserRepository(private val api: ApiService) {
    suspend fun getUser(userId: String, environment: Environment): User {
        val baseUrl = when (environment) {
            Environment.DEV -> "https://dev-api.example.com"
            Environment.STAGING -> "https://staging-api.example.com"
            Environment.PROD -> "https://api.example.com"
        }
        return api.getUser("$baseUrl/users/$userId")
    }
}

// Or use @Url with full URLs:
@GET
suspend fun fetchFromUrl(@Url fullUrl: String): ResponseBody

Multiple Base URLs in Same App:

// Different services with different base URLs
object RetrofitFactory {

    private fun createRetrofit(baseUrl: String): Retrofit {
        return Retrofit.Builder()
            .baseUrl(baseUrl)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    fun createUserService(): UserApiService {
        return createRetrofit("https://users-api.example.com/")
            .create(UserApiService::class.java)
    }

    fun createPaymentService(): PaymentApiService {
        return createRetrofit("https://payments-api.example.com/")
            .create(PaymentApiService::class.java)
    }
}

Custom Converters:

// Custom JSON parsing with Moshi (faster than Gson)
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .add(Date::class.java, Rfc3339DateJsonAdapter())
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(MoshiConverterFactory.create(moshi))
    .build()

// Or use kotlinx.serialization (even faster)
val json = Json {
    ignoreUnknownKeys = true
    coerceInputValues = true
}

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
    .build()

Response Wrapper Pattern:

// API always returns: { "data": ..., "error": ..., "code": 200 }
data class ApiResponse<T>(
    val data: T?,
    val error: String?,
    val code: Int
)

// Retrofit interface
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") String): ApiResponse<User>
}

// Repository unwraps
class UserRepository(private val api: ApiService) {
    suspend fun getUser(userId: String): Result<User> {
        return try {
            val response = api.getUser(userId)
            if (response.data != null) {
                Result.success(response.data)
            } else {
                Result.failure(Exception(response.error ?: "Unknown error"))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

Call Adapter for Result/Resource:

// Custom CallAdapter to wrap responses automatically
class ResultCallAdapterFactory : CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        // Check if return type is Result<T>
        if (getRawType(returnType) != Result::class.java) {
            return null
        }

        val successType = getParameterUpperBound(0, returnType as ParameterizedType)
        return ResultCallAdapter<Any>(successType)
    }
}

class ResultCallAdapter<T>(
    private val successType: Type
) : CallAdapter<T, Result<T>> {

    override fun responseType(): Type = successType

    override fun adapt(call: Call<T>): Result<T> {
        return try {
            val response = call.execute()
            if (response.isSuccessful) {
                Result.success(response.body()!!)
            } else {
                Result.failure(HttpException(response))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Usage
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addCallAdapterFactory(ResultCallAdapterFactory())
    .addConverterFactory(GsonConverterFactory.create())
    .build()

interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") String): Result<User>  // Automatically wrapped!
}

GraphQL with Retrofit:

// GraphQL queries as POST requests
data class GraphQLRequest(
    val query: String,
    val variables: Map<String, Any>? = null
)

data class GraphQLResponse<T>(
    val data: T?,
    val errors: List<GraphQLError>?
)

data class GraphQLError(
    val message: String,
    val locations: List<Location>?
)

interface GraphQLApiService {
    @POST("graphql")
    suspend fun <T> query(@Body request: GraphQLRequest): GraphQLResponse<T>
}

// Usage
class UserRepository(private val api: GraphQLApiService) {
    suspend fun getUser(userId: String): User? {
        val query = """
            query GetUser(${"$"}id: ID!) {
                user(id: ${"$"}id) {
                    id
                    name
                    email
                }
            }
        """.trimIndent()

        val request = GraphQLRequest(
            query = query,
            variables = mapOf("id" to userId)
        )

        val response = api.query<UserData>(request)
        return response.data?.user
    }
}

data class UserData(val user: User)

File Upload with Progress:

// Multipart file upload
interface ApiService {
    @Multipart
    @POST("upload")
    suspend fun uploadImage(
        @Part("description") description: RequestBody,
        @Part image: MultipartBody.Part
    ): UploadResponse
}

// Usage with progress tracking
class FileRepository(private val api: ApiService) {

    suspend fun uploadImage(
        file: File,
        description: String,
        onProgress: (Int) -> Unit
    ): UploadResponse {

        // Create request body with progress tracking
        val requestFile = file.asRequestBody("image/jpeg".toMediaType())

        val progressRequestBody = object : RequestBody() {
            override fun contentType() = requestFile.contentType()

            override fun contentLength() = requestFile.contentLength()

            override fun writeTo(sink: BufferedSink) {
                val fileLength = contentLength()
                val buffer = ByteArray(8192)
                var uploaded = 0L

                requestFile.contentLength()
                file.inputStream().use { input ->
                    var read: Int
                    while (input.read(buffer).also { read = it } != -1) {
                        uploaded += read
                        sink.write(buffer, 0, read)

                        // Report progress
                        val progress = (100 * uploaded / fileLength).toInt()
                        onProgress(progress)
                    }
                }
            }
        }

        val part = MultipartBody.Part.createFormData(
            "image",
            file.name,
            progressRequestBody
        )

        val descriptionBody = description.toRequestBody("text/plain".toMediaType())

        return api.uploadImage(descriptionBody, part)
    }
}

Download with Progress:

interface ApiService {
    @Streaming  // Important: prevents loading entire file in memory
    @GET
    suspend fun downloadFile(@Url fileUrl: String): ResponseBody
}

class FileRepository(private val api: ApiService) {

    suspend fun downloadFile(
        url: String,
        destinationFile: File,
        onProgress: (Int) -> Unit
    ) {
        val response = api.downloadFile(url)

        response.byteStream().use { input ->
            destinationFile.outputStream().use { output ->
                val totalBytes = response.contentLength()
                val buffer = ByteArray(8192)
                var downloaded = 0L
                var read: Int

                while (input.read(buffer).also { read = it } != -1) {
                    output.write(buffer, 0, read)
                    downloaded += read

                    val progress = if (totalBytes > 0) {
                        (100 * downloaded / totalBytes).toInt()
                    } else {
                        -1  // Unknown size
                    }
                    onProgress(progress)
                }

                output.flush()
            }
        }
    }
}

Query Map for Dynamic Parameters:

interface ApiService {
    @GET("search")
    suspend fun search(
        @QueryMap filters: Map<String, String>
    ): SearchResults
}

// Usage - add parameters dynamically
fun buildFilters(): Map<String, String> {
    return buildMap {
        put("query", "kotlin")
        if (priceMin != null) put("price_min", priceMin.toString())
        if (priceMax != null) put("price_max", priceMax.toString())
        if (category != null) put("category", category)
        // ... add more filters dynamically
    }
}

val results = api.search(buildFilters())
// GET /search?query=kotlin&price_min=100&price_max=500&category=books

Header Map for Dynamic Headers:

interface ApiService {
    @GET("data")
    suspend fun getData(
        @HeaderMap headers: Map<String, String>
    ): Data
}

// Usage
fun buildHeaders(): Map<String, String> {
    return buildMap {
        put("Authorization", "Bearer $token")
        put("Accept-Language", currentLocale)
        put("User-Agent", "MyApp/${BuildConfig.VERSION_NAME}")
        if (experimentId != null) {
            put("X-Experiment-Id", experimentId)
        }
    }
}

Coroutine Integration - Best Patterns:

// 1. Simple suspend function
interface ApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") String): User
}

// 2. Flow for streaming/polling
interface ApiService {
    @GET("updates")
    fun getUpdates(): Flow<Update>
}

// Implementation in repository
fun getUpdates(): Flow<Update> = flow {
    while (true) {
        val update = api.getUpdatesNow()  // Regular suspend call
        emit(update)
        delay(5000)  // Poll every 5 seconds
    }
}.flowOn(Dispatchers.IO)

// 3. Deferred for parallel requests
interface ApiService {
    @GET("user/profile")
    fun getUserProfile(): Deferred<Profile>

    @GET("user/posts")
    fun getUserPosts(): Deferred<List<Post>>
}

// Parallel execution
suspend fun loadUserData() = coroutineScope {
    val profile = async { api.getUserProfile().await() }
    val posts = async { api.getUserPosts().await() }

    UserData(
        profile = profile.await(),
        posts = posts.await()
    )
}

Testing Retrofit APIs:

// Mock API for testing
class MockApiService : ApiService {
    override suspend fun getUser(id: String): User {
        delay(100)  // Simulate network delay
        return User(id, "Test User", "test@example.com")
    }
}

// Or use MockWebServer
@Test
fun testApiCall() = runTest {
    val mockWebServer = MockWebServer()
    mockWebServer.enqueue(
        MockResponse()
            .setResponseCode(200)
            .setBody("""{"id":"123","name":"Test"}""")
    )
    mockWebServer.start()

    val retrofit = Retrofit.Builder()
        .baseUrl(mockWebServer.url("/"))
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val api = retrofit.create(ApiService::class.java)
    val user = api.getUser("123")

    assertEquals("Test", user.name)

    mockWebServer.shutdown()
}

Retrofit Best Practices:

  1. Singleton Retrofit instance per base URL
  2. Use suspend functions with coroutines
  3. @Streaming for large downloads
  4. Custom CallAdapter for Result wrappers
  5. QueryMap/HeaderMap for dynamic parameters
  6. Proper error handling with sealed classes
  7. Testing with MockWebServer

Connection Pooling and Keep-Alive

Q: Explain HTTP Keep-Alive and connection pooling in detail.

Complete Answer:

Connection pooling and Keep-Alive are critical for performance on mobile networks. They reduce latency by reusing TCP/TLS connections.

HTTP Keep-Alive - How It Works:

Without Keep-Alive (HTTP/1.0 default):
Request 1:
  1. TCP 3-way handshake (1 RTT)
  2. TLS handshake (2 RTT)
  3. HTTP request/response
  4. Close connection

Request 2:
  1. TCP 3-way handshake (1 RTT)
  2. TLS handshake (2 RTT)
  3. HTTP request/response
  4. Close connection

Total overhead: 6 RTT (round trips)

With Keep-Alive (HTTP/1.1 default):
Request 1:
  1. TCP 3-way handshake (1 RTT)
  2. TLS handshake (2 RTT)
  3. HTTP request/response
  (Connection stays open)

Request 2:
  3. HTTP request/response (reuse connection!)

Request 3:
  3. HTTP request/response (reuse connection!)

Total overhead: 3 RTT (50% reduction!)

Keep-Alive Headers:

Client sends:
Connection: keep-alive

Server responds:
Connection: keep-alive
Keep-Alive: timeout=5, max=100

Meaning:
- Connection stays open for 5 seconds idle
- Maximum 100 requests on this connection

OkHttp Connection Pool Details:

// OkHttp manages connection pooling automatically
val connectionPool = ConnectionPool(
    maxIdleConnections = 5,     // Max idle connections to keep
    keepAliveDuration = 5,       // How long to keep them (minutes)
    timeUnit = TimeUnit.MINUTES
)

val client = OkHttpClient.Builder()
    .connectionPool(connectionPool)
    .build()

// How it works internally:
// 1. Make request to api.example.com
// 2. Open TCP + TLS connection
// 3. Send HTTP request, get response
// 4. Instead of closing, add to pool
// 5. Next request to api.example.com → reuse from pool!
// 6. After 5 minutes idle → close connection

Connection Pool Behavior:

// Example: 3 requests to same server
val client = OkHttpClient()  // Has default connection pool

// Request 1
val request1 = Request.Builder()
    .url("https://api.example.com/users/1")
    .build()

client.newCall(request1).execute().use { response ->
    // Opens new connection
    // After response, connection goes to pool
}

// Request 2 (immediately after)
val request2 = Request.Builder()
    .url("https://api.example.com/users/2")
    .build()

client.newCall(request2).execute().use { response ->
    // Reuses connection from pool (FAST!)
    // No TCP handshake, no TLS handshake
}

// Request 3 (6 minutes later)
delay(6.minutes)
val request3 = Request.Builder()
    .url("https://api.example.com/users/3")
    .build()

client.newCall(request3).execute().use { response ->
    // Connection was closed (>5 min idle)
    // Opens new connection
}

Per-Host Connection Limits:

val dispatcher = Dispatcher().apply {
    maxRequests = 64              // Total concurrent requests
    maxRequestsPerHost = 5        // Per domain
}

val client = OkHttpClient.Builder()
    .dispatcher(dispatcher)
    .build()

// Why limit per host?
// - Prevents overwhelming single server
// - Distributes load across multiple hosts
// - Mobile networks handle this better

Monitoring Connection Pool:

class ConnectionPoolMonitor : EventListener() {

    override fun connectionAcquired(call: Call, connection: Connection) {
        Log.d("Pool", "Connection acquired from pool: $connection")
    }

    override fun connectionReleased(call: Call, connection: Connection) {
        Log.d("Pool", "Connection returned to pool: $connection")
    }

    override fun connectStart(
        call: Call,
        inetSocketAddress: InetSocketAddress,
        proxy: Proxy
    ) {
        Log.d("Pool", "Opening new connection (pool miss)")
    }
}

val client = OkHttpClient.Builder()
    .eventListenerFactory { ConnectionPoolMonitor() }
    .build()

Connection Pool Statistics:

val client = OkHttpClient()

// Get connection pool stats
fun printPoolStats() {
    val pool = client.connectionPool
    Log.d("Pool", "Connection count: ${pool.connectionCount()}")
    Log.d("Pool", "Idle connections: ${pool.idleConnectionCount()}")
}

// Clean up idle connections manually (rare)
client.connectionPool.evictAll()

HTTP/2 Multiplexing vs Keep-Alive:

HTTP/1.1 with Keep-Alive:
Connection 1: Request A → Response A → Request B → Response B
(Sequential on same connection)

HTTP/2 Multiplexing:
Connection 1:
  Stream 1: Request A → Response A (interleaved)
  Stream 2: Request B → Response B (interleaved)
  Stream 3: Request C → Response C (interleaved)
(Concurrent on same connection!)

Best Practices:

// 1. Reuse OkHttpClient instances
// ❌ BAD: Creates new connection pool each time
fun makeRequest() {
    val client = OkHttpClient()  // New pool!
    // ...
}

// ✅ GOOD: Singleton client with shared pool
object NetworkClient {
    val okHttpClient = OkHttpClient()
}

// 2. Configure pool for your use case
// Heavy API usage:
val heavyPool = ConnectionPool(
    maxIdleConnections = 10,
    keepAliveDuration = 10,
    timeUnit = TimeUnit.MINUTES
)

// Light API usage:
val lightPool = ConnectionPool(
    maxIdleConnections = 2,
    keepAliveDuration = 2,
    timeUnit = TimeUnit.MINUTES
)

// 3. Clean up on low memory
override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)
    if (level >= TRIM_MEMORY_RUNNING_LOW) {
        client.connectionPool.evictAll()
        client.cache?.evictAll()
    }
}

TCP Fast Open (TFO):

Traditional TCP:
Client → SYN
Server → SYN-ACK
Client → ACK + HTTP request (3 RTT)

TCP Fast Open:
Client → SYN + HTTP request (in cookie)
Server → SYN-ACK + HTTP response (2 RTT!)

// Enable TFO (Android 4.4+, mostly transparent)
// OkHttp uses it automatically when available

Persistent Connection Issues:

// Problem: Server closes connection silently
// Solution: OkHttp detects and retries automatically

class RetryInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        var request = chain.request()
        var response = chain.proceed(request)
        var tryCount = 0

        while (!response.isSuccessful && tryCount < 3) {
            tryCount++
            response.close()

            // Retry with new connection
            response = chain.proceed(request)
        }

        return response
    }
}

Performance Impact:

Measured on 3G network (200ms latency):

Without pooling:
- 10 requests: 6 seconds (TCP+TLS overhead each time)
- Battery: 100 units

With pooling:
- 10 requests: 2.5 seconds (reuse connections)
- Battery: 45 units (55% savings!)

With HTTP/2 multiplexing:
- 10 requests: 1.8 seconds
- Battery: 35 units

Network Security

Q: Explain network security best practices for Android apps.

Complete Answer:

Network security is critical for protecting user data. Android provides multiple layers of security.

1. HTTPS Enforcement:

// Always use HTTPS, never HTTP
// ❌ BAD
val url = "http://api.example.com/users"  // Unencrypted!

// ✅ GOOD
val url = "https://api.example.com/users"  // TLS encrypted

// Android 9+ blocks cleartext traffic by default
// In AndroidManifest.xml:
android:usesCleartextTraffic="false"  // Explicit (default)

2. Network Security Config:

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- Default config for all connections -->
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <!-- Trust system certificates -->
            <certificates src="system" />
        </trust-anchors>
    </base-config>

    <!-- Specific domain configurations -->
    <domain-config>
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2026-01-01">
            <!-- Certificate pinning -->
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
            <!-- Backup pin -->
            <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
        </pin-set>
    </domain-config>

    <!-- Debug overrides (dev environment only) -->
    <debug-overrides>
        <trust-anchors>
            <!-- Trust custom CA for debugging (Charles, Proxyman) -->
            <certificates src="user" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

<!-- In AndroidManifest.xml -->
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

3. Certificate Pinning:

// Pin server certificate to prevent MITM attacks
val certificatePinner = CertificatePinner.Builder()
    .add(
        "api.example.com",
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
    )
    .add(
        "api.example.com",
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="  // Backup!
    )
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

// What this does:
// 1. Client connects to server
// 2. Server sends certificate
// 3. OkHttp hashes certificate public key
// 4. Compares hash to pinned values
// 5. If no match → SSLPeerUnverifiedException
// 6. If match → Connection established

Get Certificate Pins:

# Method 1: Using openssl
echo | openssl s_client -connect api.example.com:443 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
openssl enc -base64

# Method 2: Using OkHttp (temporary)
val client = OkHttpClient.Builder()
    .certificatePinner(
        CertificatePinner.Builder()
            .add("api.example.com", "sha256/DUMMY")  // Will fail and show correct pins
            .build()
    )
    .build()

// Run request → Will crash with correct pins in error message

4. TLS Version Enforcement:

// Enforce TLS 1.2+ (TLS 1.0/1.1 are deprecated and insecure)
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
    .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
    .build()

val client = OkHttpClient.Builder()
    .connectionSpecs(listOf(spec))
    .build()

5. Custom Trust Manager (Use Carefully):

// Only for specific use cases (e.g., self-signed certs in dev)
// ⚠️ NEVER disable SSL verification in production!

fun createUnsafeClient(): OkHttpClient {
    val trustAllCerts = arrayOf<TrustManager>(
        object : X509TrustManager {
            override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}
            override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}
            override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
        }
    )

    val sslContext = SSLContext.getInstance("TLS")
    sslContext.init(null, trustAllCerts, SecureRandom())

    return OkHttpClient.Builder()
        .sslSocketFactory(sslContext.socketFactory, trustAllCerts[0] as X509TrustManager)
        .hostnameVerifier { _, _ -> true }  // ⚠️ DANGEROUS!
        .build()
}

// Only use in debug builds:
val client = if (BuildConfig.DEBUG) {
    createUnsafeClient()
} else {
    OkHttpClient()  // Secure default
}

6. API Key Security:

// ❌ BAD: Hardcoded API key
const val API_KEY = "sk_live_abc123..."  // Visible in APK!

// ✅ GOOD: Use NDK/C++ to obscure (still not perfect)
// In native-lib.cpp
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_Config_getApiKey(JNIEnv *env, jobject) {
    return env->NewStringUTF("sk_live_abc123...");
}

// In Kotlin
external fun getApiKey(): String

// ✅ BETTER: Use server-side proxy
// App → Your server → Third-party API
// API key stays on server, never in app

// ✅ BEST: Use OAuth / temporary tokens
// App gets short-lived token from your server
// Token expires, limited scope

7. Request/Response Encryption:

// For extra-sensitive data, encrypt at application level
class EncryptionInterceptor(private val cipher: Cipher) : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val original = chain.request()

        // Encrypt request body
        val encryptedBody = original.body?.let { body ->
            val buffer = Buffer()
            body.writeTo(buffer)
            val encrypted = cipher.doFinal(buffer.readByteArray())
            encrypted.toRequestBody(body.contentType())
        }

        val encryptedRequest = original.newBuilder()
            .method(original.method, encryptedBody)
            .build()

        val response = chain.proceed(encryptedRequest)

        // Decrypt response body
        val decryptedBody = response.body?.bytes()?.let { encrypted ->
            val decrypted = cipher.doFinal(encrypted)
            decrypted.toResponseBody(response.body?.contentType())
        }

        return response.newBuilder()
            .body(decryptedBody)
            .build()
    }
}

8. ProGuard/R8 Obfuscation:

# proguard-rules.pro

# Keep Retrofit/OkHttp classes
-keep class retrofit2.** { *; }
-keep class okhttp3.** { *; }

# Keep API models (or use @Keep annotation)
-keep class com.example.app.api.models.** { *; }

# Remove logging in release
-assumenosideeffects class android.util.Log {
    public static *** d(...);
    public static *** v(...);
    public static *** i(...);
}

9. Root Detection:

// Detect rooted devices (additional security layer)
fun isDeviceRooted(): Boolean {
    // Check for su binary
    val paths = arrayOf(
        "/system/app/Superuser.apk",
        "/sbin/su",
        "/system/bin/su",
        "/system/xbin/su"
    )

    for (path in paths) {
        if (File(path).exists()) return true
    }

    // Check for test keys
    val buildTags = Build.TAGS
    if (buildTags != null && buildTags.contains("test-keys")) {
        return true
    }

    return false
}

// In sensitive operations
if (isDeviceRooted()) {
    // Warn user or limit functionality
    showRootWarning()
}

10. SSL Pinning Failure Handling:

class SafeCertificatePinner(
    private val certificatePinner: CertificatePinner,
    private val onPinningFailure: () -> Unit
) {

    fun check(hostname: String, peerCertificates: List<Certificate>) {
        try {
            certificatePinner.check(hostname, peerCertificates)
        } catch (e: SSLPeerUnverifiedException) {
            // Log to analytics
            logSecurityEvent("SSL_PINNING_FAILURE", hostname)

            // Notify user
            onPinningFailure()

            // Re-throw to prevent connection
            throw e
        }
    }
}

Security Checklist:

✅ Always use HTTPS (never HTTP) ✅ Implement certificate pinning for critical APIs ✅ Use Network Security Config ✅ Enforce TLS 1.2+ ✅ Never hardcode API keys ✅ Obfuscate with ProGuard/R8 ✅ Test with MITM proxy (Charles, Proxyman) ✅ Handle root detection ✅ Monitor SSL pinning failures ✅ Use short-lived tokens (not permanent keys)


WebSocket Implementation

Q: How do you implement WebSockets in Android for real-time communication?

Complete Answer:

WebSockets represent a fundamental shift in how we think about client-server communication, and understanding them deeply is important for any senior engineer working on real-time features. Let me explain not just how to implement them, but why they exist and when they're the right choice.

The Fundamental Problem WebSockets Solve:

Traditional HTTP is request-response. Your client asks for something, the server answers, and that's it. If you want real-time updates - like new messages in a chat app, live sports scores, stock prices, or collaborative editing - you have a problem. The server can't initiate communication; it can only respond to requests.

The naive solution is polling: make a request every few seconds asking "got anything new?" This is wasteful. Most of the time the answer is "no," but you've still paid the cost of a full HTTP request-response cycle. On mobile, this burns battery and data because you're constantly waking up the radio for mostly-empty responses.

Long polling is better: make a request and the server holds it open until there's new data or a timeout occurs. When data arrives, the server responds, the client processes it, and immediately makes a new long-polling request. This reduces wasted requests, but you still have HTTP overhead on every cycle, and there's latency between when data arrives and when the current long-poll request times out and a new one establishes.

WebSockets solve this by upgrading a connection to a persistent, bidirectional channel. After the initial HTTP handshake, the connection stays open indefinitely. Both client and server can send messages at any time. No request-response cycle. No polling. Just pure message exchange.

The WebSocket Handshake - How Upgrade Works:

The WebSocket connection starts as an HTTP request, but a special one. Your client sends an HTTP request with "Upgrade: websocket" and "Connection: Upgrade" headers, plus a randomly generated key. This is asking the server "can we turn this HTTP connection into a WebSocket?"

If the server supports WebSockets, it responds with HTTP 101 Switching Protocols, includes a cryptographic hash of your key to prove it's a legitimate WebSocket server, and from that point forward, the connection is no longer HTTP - it's WebSocket protocol. Both sides can now send and receive messages freely.

This upgrade mechanism is clever because it works with existing HTTP infrastructure. Proxies and load balancers that understand HTTP can establish the connection, then step aside and just pass bytes back and forth once it's upgraded.

WebSocket vs SSE vs Long Polling:

Before diving deeper into implementation, let me clarify when you'd choose WebSockets over alternatives, because this is a common interview question.

Server-Sent Events (SSE) is simpler than WebSockets but only supports server-to-client messages, not client-to-server. It's perfect for things like live feeds or notifications where you only need the server to push updates, and any client-to-server communication can happen via normal API calls. SSE is easier to implement and works better with HTTP/2 multiplexing.

Long polling works everywhere and is simple but inefficient. It's a fallback when WebSockets aren't available, but you'd never choose it as your primary approach if WebSockets are an option.

WebSockets are the right choice when you need bidirectional communication with low latency. Chat applications are the classic example - users send messages, receive messages, need typing indicators, read receipts, etc. Collaborative editing tools are another perfect fit - multiple users making changes simultaneously.

Connection Management - The Hard Part:

Here's where WebSocket implementation gets tricky: connection management. In a perfect world, your WebSocket connects, stays connected forever, and everyone's happy. In reality, connections die constantly. Mobile networks switch between cell towers. Users lose signal in tunnels. They switch from WiFi to cellular. Background apps get their network access throttled. Intermediate proxies timeout idle connections.

A production WebSocket implementation needs bulletproof reconnection logic. When the connection drops, you need to detect it quickly, attempt to reconnect, and handle the reconnection gracefully. "Quickly" is important - you don't want users to wait 30 seconds to realize they're disconnected.

The detection challenge is that sometimes you don't know a connection died. The TCP connection might be in a half-open state where your side thinks it's connected but the other side is gone. This is why WebSocket implementations use heartbeats - periodic ping/pong messages that verify the connection is alive. If you send a ping and don't receive a pong within a reasonable time, you know the connection is dead and can reconnect.

Exponential Backoff - The Right Way to Reconnect:

When reconnecting, never use a fixed retry interval. If you always wait one second and retry, and your server is having issues, you'll hammer it with reconnection attempts from thousands of clients every second. This can actually prevent recovery.

Instead, use exponential backoff with jitter. First reconnect attempt happens after one second. If that fails, wait two seconds. Then four, eight, sixteen, maxing out at something like thirty seconds. The "jitter" part means adding randomness - instead of exactly two seconds, wait between 1.5 and 2.5 seconds. This prevents all clients from reconnecting in perfect synchronization.

The exponential backoff gives struggling servers time to recover while still reconnecting quickly when it's just a transient network issue. The jitter prevents thundering herd problems where all clients reconnect simultaneously.

Message Queuing - Don't Lose Messages:

Another critical consideration: what happens to messages sent while disconnected? If a user types a chat message but their connection is down, you can't just drop that message. You need a queue.

When the connection is alive, messages go straight through. When disconnected, messages go into a local queue. When reconnecting succeeds, drain the queue before accepting new messages. This ensures in-order delivery even across connection failures.

But queues can grow unbounded if disconnection lasts a long time. You need a policy: maybe you limit the queue to the last hundred messages, or you persist the queue to disk so it survives app restarts, or you have a maximum queue size and reject new messages when full. The right policy depends on your use case.

Authentication and Security:

WebSockets don't have a built-in authentication mechanism. The initial HTTP handshake is your opportunity to authenticate. You can include an authorization header or pass a token as a query parameter. Once upgraded to WebSocket, that initial authentication is all you get.

For long-lived connections, this creates a problem: if your auth token expires, you can't easily refresh it over the WebSocket. Some systems include an application-level authentication message sent after the WebSocket connects. Others reauthenticate by closing the WebSocket, getting a new token via normal API calls, then reconnecting with the new token.

Security-wise, always use WSS (WebSocket Secure) not WS. This is WebSocket over TLS, like HTTPS vs HTTP. Unencrypted WebSockets expose all your messages to anyone on the network path.

Message Framing and Protocol:

WebSockets send data in frames. Each frame has a small header indicating the frame type (text or binary), whether it's the final frame of a message, and the payload length. Your application messages can be split across multiple frames, and WebSocket handles reassembly.

Most apps use text frames containing JSON for messages. This is simple and debuggable. For high-throughput applications or when bandwidth matters, binary frames with a compact format like Protocol Buffers or MessagePack can reduce data transfer significantly.

Define a clear message protocol. Don't just send arbitrary JSON - define message types with version numbers, schemas, and error codes. Your protocol should handle acknowledgments, errors, and edge cases like duplicate messages or out-of-order delivery.

Handling Backpressure:

What happens if messages arrive faster than you can process them? Without backpressure handling, your app will accumulate a backlog in memory, eventually running out and crashing.

WebSocket protocol has flow control mechanisms, but they're at the frame level. Application-level backpressure is your responsibility. Solutions include dropping old messages (for real-time data where only the latest matters), buffering to disk, or sending a message to the server asking it to slow down.

State Synchronization After Reconnect:

When reconnecting, your client and server states might have diverged. The server might have sent messages you didn't receive. Your client might have queued messages it hasn't sent yet. Both sides need to resynchronize.

One approach is for the client to send a "last received message ID" when reconnecting, and the server re-sends anything after that. Another is to treat reconnection as a full state refresh - the client asks for current state and reconciles with local state.

For chat apps, this might mean requesting recent message history. For collaborative editing, it might mean getting a diff of changes since you disconnected. The specific approach depends on your data model.

Testing WebSockets:

Testing WebSocket implementations is challenging because you need to simulate realistic failure scenarios. Unit tests can mock the WebSocket, but they won't catch real-world issues. Integration tests should simulate connection drops, server failures, slow networks, and reconnection scenarios.

Tools like Charles Proxy can throttle or drop connections to test reconnection logic. Android's Network Profiler can simulate poor network conditions. Don't just test on WiFi - test on actual mobile networks where conditions vary.

When WebSockets Are Wrong:

Let me close with when NOT to use WebSockets. If communication is primarily client-initiated with occasional server responses, normal HTTP is fine. If you only need server-to-client updates, SSE is simpler. If updates can tolerate minutes of delay, push notifications are more battery-efficient than a persistent WebSocket.

WebSockets require persistent connections, which consumes battery and network resources. They're harder to scale on the server side than stateless HTTP. They have complex failure modes. Use them when you genuinely need real-time bidirectional communication, not because they seem cool.

OkHttp WebSocket Implementation:

class WebSocketManager(
    private val url: String,
    private val client: OkHttpClient = OkHttpClient()
) {

    private var webSocket: WebSocket? = null
    private var listener: WebSocketListener? = null

    fun connect(messageListener: (String) -> Unit) {
        val request = Request.Builder()
            .url(url)
            .build()

        listener = object : WebSocketListener() {

            override fun onOpen(webSocket: WebSocket, response: Response) {
                Log.d("WebSocket", "Connected to $url")
                // Send initial message if needed
                webSocket.send("""{"type":"subscribe","channel":"updates"}""")
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                Log.d("WebSocket", "Received: $text")
                messageListener(text)
            }

            override fun onMessage(webSocket: WebSocket, bytes: ByteString) {
                Log.d("WebSocket", "Received bytes: ${bytes.hex()}")
                messageListener(bytes.utf8())
            }

            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                Log.d("WebSocket", "Closing: $code / $reason")
                webSocket.close(NORMAL_CLOSURE_STATUS, null)
            }

            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                Log.d("WebSocket", "Closed: $code / $reason")
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                Log.e("WebSocket", "Error: ${t.message}")
                // Implement reconnection logic
                scheduleReconnect()
            }
        }

        webSocket = client.newWebSocket(request, listener!!)
    }

    fun send(message: String): Boolean {
        return webSocket?.send(message) ?: false
    }

    fun disconnect() {
        webSocket?.close(NORMAL_CLOSURE_STATUS, "Client closing")
        webSocket = null
    }

    private fun scheduleReconnect() {
        // Implement exponential backoff
    }

    companion object {
        private const val NORMAL_CLOSURE_STATUS = 1000
    }
}

Production WebSocket with Reconnection:

class RobustWebSocketClient(
    private val url: String,
    private val client: OkHttpClient
) {

    private var webSocket: WebSocket? = null
    private var reconnectAttempts = 0
    private var isIntentionalClose = false

    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

    // Message channels
    private val _messages = MutableSharedFlow<String>()
    val messages: SharedFlow<String> = _messages.asSharedFlow()

    private val _connectionState = MutableStateFlow(ConnectionState.DISCONNECTED)
    val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()

    fun connect() {
        isIntentionalClose = false
        reconnectAttempts = 0
        connectInternal()
    }

    private fun connectInternal() {
        if (_connectionState.value == ConnectionState.CONNECTING) {
            return  // Already connecting
        }

        _connectionState.value = ConnectionState.CONNECTING

        val request = Request.Builder()
            .url(url)
            .build()

        webSocket = client.newWebSocket(request, object : WebSocketListener() {

            override fun onOpen(webSocket: WebSocket, response: Response) {
                _connectionState.value = ConnectionState.CONNECTED
                reconnectAttempts = 0

                // Send heartbeat
                startHeartbeat(webSocket)
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                scope.launch {
                    _messages.emit(text)
                }
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                _connectionState.value = ConnectionState.DISCONNECTED

                if (!isIntentionalClose) {
                    scheduleReconnect()
                }
            }

            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                _connectionState.value = ConnectionState.DISCONNECTED

                if (!isIntentionalClose && code != 1000) {
                    scheduleReconnect()
                }
            }
        })
    }

    private fun scheduleReconnect() {
        reconnectAttempts++

        // Exponential backoff: 1s, 2s, 4s, 8s, max 30s
        val delay = min(2.0.pow(reconnectAttempts).toLong() * 1000, 30000)

        Log.d("WebSocket", "Reconnecting in ${delay}ms (attempt $reconnectAttempts)")

        scope.launch {
            delay(delay)
            if (!isIntentionalClose) {
                connectInternal()
            }
        }
    }

    private fun startHeartbeat(webSocket: WebSocket) {
        scope.launch {
            while (webSocket.isOpen()) {
                webSocket.send("""{"type":"ping"}""")
                delay(30_000)  // 30 seconds
            }
        }
    }

    fun send(message: String) {
        webSocket?.send(message)
    }

    fun disconnect() {
        isIntentionalClose = true
        webSocket?.close(1000, "Client closing")
        scope.cancel()
    }

    private fun WebSocket.isOpen(): Boolean {
        return _connectionState.value == ConnectionState.CONNECTED
    }
}

enum class ConnectionState {
    CONNECTED,
    CONNECTING,
    DISCONNECTED
}

Chat App Example:

data class ChatMessage(
    val id: String,
    val userId: String,
    val text: String,
    val timestamp: Long
)

class ChatRepository(
    private val webSocketClient: RobustWebSocketClient,
    private val gson: Gson
) {

    val messages: Flow<ChatMessage> = webSocketClient.messages
        .map { json ->
            gson.fromJson(json, ChatMessage::class.java)
        }
        .catch { error ->
            Log.e("Chat", "Parse error: ${error.message}")
        }

    val connectionState = webSocketClient.connectionState

    fun connect() {
        webSocketClient.connect()
    }

    fun sendMessage(text: String) {
        val message = ChatMessage(
            id = UUID.randomUUID().toString(),
            userId = getCurrentUserId(),
            text = text,
            timestamp = System.currentTimeMillis()
        )

        val json = gson.toJson(message)
        webSocketClient.send(json)
    }

    fun disconnect() {
        webSocketClient.disconnect()
    }

    private fun getCurrentUserId(): String = "user123"  // From auth
}

// ViewModel
class ChatViewModel(
    private val repository: ChatRepository
) : ViewModel() {

    val messages = repository.messages
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = null
        )

    val connectionState = repository.connectionState
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = ConnectionState.DISCONNECTED
        )

    init {
        repository.connect()
    }

    fun sendMessage(text: String) {
        repository.sendMessage(text)
    }

    override fun onCleared() {
        repository.disconnect()
        super.onCleared()
    }
}

WebSocket with Authentication:

// Send auth token after connection
override fun onOpen(webSocket: WebSocket, response: Response) {
    val authMessage = """
        {
            "type": "auth",
            "token": "${getAuthToken()}"
        }
    """.trimIndent()

    webSocket.send(authMessage)
}

// Or use query parameter in URL
val url = "wss://api.example.com/chat?token=${getAuthToken()}"

WebSocket Compression:

// OkHttp supports per-message deflate extension
val client = OkHttpClient.Builder()
    .build()  // Compression enabled by default

// Server must support: permessage-deflate

Testing WebSockets:

@Test
fun testWebSocketConnection() = runTest {
    val mockWebServer = MockWebServer()
    mockWebServer.start()

    // Create test WebSocket client
    val client = OkHttpClient()
    val url = mockWebServer.url("/chat").toString()
        .replace("http://", "ws://")

    var received: String? = null

    val request = Request.Builder().url(url).build()
    val webSocket = client.newWebSocket(request, object : WebSocketListener() {
        override fun onMessage(webSocket: WebSocket, text: String) {
            received = text
        }
    })

    // Simulate server message
    val serverSocket = mockWebServer.acceptWebSocket()
    serverSocket?.send("Hello from server")

    delay(100)
    assertEquals("Hello from server", received)

    webSocket.close(1000, null)
    mockWebServer.shutdown()
}

Best Practices:

  1. Reconnection logic with exponential backoff
  2. Heartbeat/ping to detect dead connections
  3. State management (connecting, connected, disconnected)
  4. Authentication before sending sensitive data
  5. Error handling for parse failures
  6. Lifecycle aware (disconnect when app backgrounded)
  7. Message queue for offline messages

When to Use WebSockets:

✅ Chat applications ✅ Real-time notifications ✅ Live sports scores ✅ Collaborative editing ✅ Stock tickers ✅ Gaming

❌ Simple request/response (use HTTP) ❌ Infrequent updates (use polling or FCM)


Quick Reference Summary

HTTP/2 Benefits:

  • Multiplexing (multiple requests, one connection)
  • Header compression (70-80% reduction)
  • Binary protocol (faster parsing)
  • Server push (rarely used on mobile)

OkHttp Key Features:

  • Connection pooling (reuse connections)
  • Interceptors (modify requests/responses)
  • EventListener (monitor lifecycle)
  • Certificate pinning (prevent MITM)
  • HTTP/2 and WebSocket support

Retrofit Advanced:

  • Dynamic base URLs (@Url)
  • Custom converters (Moshi, kotlinx.serialization)
  • CallAdapter for Result wrappers
  • File upload/download with progress
  • Query/Header maps for dynamic parameters

Connection Pooling:

  • Reduces latency (no handshake overhead)
  • Default: 5 connections, 5 minutes keep-alive
  • HTTP/2 multiplexing even better

Security:

  • Always HTTPS
  • Certificate pinning for critical APIs
  • Network Security Config
  • TLS 1.2+ enforcement
  • Never hardcode API keys

WebSockets:

  • Full-duplex, persistent connection
  • OkHttp built-in support
  • Implement reconnection with backoff
  • Use for real-time features (chat, live updates)

Interview Tips

Common Questions:

  • "Explain HTTP/2 advantages" → Multiplexing, header compression
  • "How does connection pooling work?" → Reuse TCP/TLS connections
  • "Implement file upload with progress" → RequestBody with progress callback
  • "Explain certificate pinning" → Pin public key hash, prevent MITM
  • "WebSocket vs HTTP" → Bidirectional, persistent vs request-response

Demonstrate Depth:

  • Mention specific OkHttp internals (interceptor chain)
  • Discuss performance metrics (RTT reduction)
  • Show security awareness (pinning, TLS versions)
  • Explain trade-offs (pinning vs certificate rotation)

Good luck with your networking interviews! 🚀

results matching ""

    No results matching ""