Networking
`URLSession` tasks and caching, `Codable` decoding, token refresh and certificate pinning, retry with backoff, and choosing a transport for a live screen.
12 questions
JuniorTheoryVery commonHow does HTTP caching work on a mobile client via Cache-Control, ETag/If-None-Match, and what does URLCache give for free?
How does HTTP caching work on a mobile client via Cache-Control, ETag/If-None-Match, and what does URLCache give for free?
Cache-Control sets freshness — how long a response may be reused before it goes stale. ETag is a version tag the client echoes in If-None-Match, so the server can answer 304 Not Modified with no body when nothing changed. URLCache does this for URLSession, reusing fresh data and revalidating per the headers.
Common mistakes
- ✗Thinking you must implement revalidation manually instead of
URLCache - ✗Confusing
Cache-Controlfreshness withETagrevalidation - ✗Assuming a
304response still carries the full body
Follow-up questions
- →When would
no-cachediffer fromno-storein a response? - →Why might an
ephemeralsession ignore your on-disk cache?
JuniorTheoryVery commonWhat task types does URLSession offer, and how do its session configurations differ?
What task types does URLSession offer, and how do its session configurations differ?
Data tasks return the response in memory, upload tasks send a body or file with progress, and download tasks write to a resumable temp file. The default config persists cache and cookies, ephemeral keeps nothing on disk, and background continues transfers while the app is suspended.
Common mistakes
- ✗Assuming every task returns its result in memory, even downloads
- ✗Thinking
ephemeralstill writes cache to disk likedefault - ✗Expecting
backgroundtransfers to stop when the app is suspended
Follow-up questions
- →When would you choose a download task over a data task?
- →What does a
backgroundsession need to resume after a relaunch?
JuniorTheoryCommonWhat does App Transport Security enforce, what is certificate pinning, and what is pinning's operational risk?
What does App Transport Security enforce, what is certificate pinning, and what is pinning's operational risk?
App Transport Security requires HTTPS with TLS 1.2+, forward secrecy, and strong ciphers, blocking plaintext HTTP by default. Certificate pinning trusts only a specific server certificate or public key, not any CA-issued one. Its risk: rotating that pinned certificate leaves the app unable to connect until it ships an update.
Common mistakes
- ✗Thinking ATS can be globally disabled with no review consequences
- ✗Believing pinning replaces TLS rather than tightening which cert is trusted
- ✗Ignoring that certificate rotation bricks a pinned app until an update
Follow-up questions
- →How does pinning a public key survive a routine certificate renewal?
- →When is an ATS exception justified for a specific domain?
JuniorTheoryCommonWhen does Swift synthesize Codable conformance, and when do you need CodingKeys or a custom decoder?
When does Swift synthesize Codable conformance, and when do you need CodingKeys or a custom decoder?
Swift synthesizes init(from:) and encode(to:) when every stored property is itself Codable. Add a CodingKeys enum to rename or skip keys when JSON names differ from your properties, and hand-write init(from:) when decoding needs transformation, defaults, or a shape the compiler cannot infer.
Common mistakes
- ✗Believing you must always write coding methods by hand
- ✗Thinking
CodingKeysmust list every property, not just renamed ones - ✗Assuming reflection decodes any JSON without a
Codableconformance
Follow-up questions
- →How do you decode a key that is sometimes missing without a custom decoder?
- →When does adding one non-
Codableproperty break synthesis?
MiddleDesignCommonYou are building the networking layer for an iOS app that talks to a REST backend across many feature screens. Design it so a feature never touches URLSession directly: put a protocol-fronted client behind an endpoint abstraction that describes path, method, query, and body; centralize Codable decoding; and map transport, HTTP-status, and decoding failures into one typed error a screen can render. Every request must be cancellable, and a unit test must inject a fake client and assert on the request it built and the decoded model without hitting the network. How do you structure the client, the endpoint type, decoding, error mapping, and the seam that lets a test swap the real transport for a stub?
You are building the networking layer for an iOS app that talks to a REST backend across many feature screens. Design it so a feature never touches URLSession directly: put a protocol-fronted client behind an endpoint abstraction that describes path, method, query, and body; centralize Codable decoding; and map transport, HTTP-status, and decoding failures into one typed error a screen can render. Every request must be cancellable, and a unit test must inject a fake client and assert on the request it built and the decoded model without hitting the network. How do you structure the client, the endpoint type, decoding, error mapping, and the seam that lets a test swap the real transport for a stub?
Put an Endpoint value (path, method, query, body) behind a Requesting protocol. A concrete client builds the URLRequest, runs URLSession, decodes with a shared Codable decoder, and maps transport, status, and decoding failures to one typed error. Features depend on the protocol, so a test injects a fake and asserts offline.
Common mistakes
- ✗Calling
URLSessiondirectly from each screen, so nothing is testable or reusable - ✗Leaking raw transport and decoding errors to the UI instead of one typed error
- ✗Making the client a concrete singleton with no protocol seam to stub
Follow-up questions
- →Where do you inject auth headers without each endpoint knowing about tokens?
- →How does the fake client assert the exact request a feature produced?
MiddleDesignCommonA screen makes several URLSession requests against a backend that occasionally returns 503s and times out, and after an outage thousands of clients retry at once. Design a retry policy: decide which requests are safe to retry, choose a backoff schedule, add jitter, cap attempts, and avoid a thundering herd — a synchronized retry spike — that re-overloads the recovering server. Say which HTTP methods and status codes you retry versus surface immediately, how you compute the delay between attempts, why jitter matters, and how a Retry-After header changes your plan.
A screen makes several URLSession requests against a backend that occasionally returns 503s and times out, and after an outage thousands of clients retry at once. Design a retry policy: decide which requests are safe to retry, choose a backoff schedule, add jitter, cap attempts, and avoid a thundering herd — a synchronized retry spike — that re-overloads the recovering server. Say which HTTP methods and status codes you retry versus surface immediately, how you compute the delay between attempts, why jitter matters, and how a Retry-After header changes your plan.
Retry only idempotent requests (GET, PUT, DELETE) and transient failures — timeouts, 503, 429 — never a 400 or a non-idempotent POST. Back off exponentially with a capped attempt count, and add random jitter so clients do not retry in lockstep and stampede the recovering server. Honor a Retry-After header when present.
Common mistakes
- ✗Retrying non-idempotent POSTs and duplicating side effects
- ✗Using fixed-interval retries with no jitter, causing a synchronized stampede
- ✗Retrying
4xxclient errors that will never succeed
Follow-up questions
- →How do you make a POST safe to retry with an idempotency key?
- →Why does full jitter beat a fixed backoff after a mass outage?
MiddleCodeCommonWrite an async request function whose transport is protocol-injected so it can be unit-tested with no network
Write an async request function whose transport is protocol-injected so it can be unit-tested with no network
Define a Transport protocol — func data(for: URLRequest) async throws -> (Data, URLResponse) — and conform URLSession to it. The function builds the request, awaits the transport, checks status, and decodes with JSONDecoder. Production injects the real URLSession; a test injects a stub with canned data, so it runs offline.
Common mistakes
- ✗Calling
URLSession.shareddirectly, leaving nothing to stub - ✗Not checking the HTTP status before decoding the body
- ✗Making the function non-generic so each model needs its own copy
Follow-up questions
- →How does a
URLProtocolsubclass intercept requests without a real server? - →Why is parameter injection easier to test than a singleton transport?
MiddleDesignCommonFive concurrent URLSession requests all receive a 401 Unauthorized at the same moment because the access token expired. You must refresh the token exactly once — not five times — and replay all five original requests with the new token once the refresh succeeds. Design this: how do you detect the 401, coordinate a single in-flight refresh while the other four wait, replay the queued requests, and handle the case where the refresh itself fails? Explain how you avoid a refresh stampede, and what happens to a new request that arrives while a refresh is already running.
Five concurrent URLSession requests all receive a 401 Unauthorized at the same moment because the access token expired. You must refresh the token exactly once — not five times — and replay all five original requests with the new token once the refresh succeeds. Design this: how do you detect the 401, coordinate a single in-flight refresh while the other four wait, replay the queued requests, and handle the case where the refresh itself fails? Explain how you avoid a refresh stampede, and what happens to a new request that arrives while a refresh is already running.
Funnel 401s through one actor or serial queue that owns the token. The first 401 starts a single refresh; the other four suspend and enqueue their continuations instead of firing their own. On success, replay every queued request with the new token; on failure, fail them all and force re-login. A request arriving mid-refresh joins the same wait.
Common mistakes
- ✗Letting each
401trigger its own refresh, causing a refresh stampede - ✗Replaying requests with the old token because the queue was not synchronized
- ✗Ignoring refresh failure, leaving the waiting requests hung forever
Follow-up questions
- →How does an actor serialize the refresh without blocking a thread?
- →What stops a request that arrives just after the refresh started from missing it?
MiddleDesignOccasionalDesign an upload for a 500 MB video that must finish even if the user backgrounds the app or it is evicted. Requirements: use a background URLSession so the transfer continues while the app is suspended; support a resumable or multipart/chunked upload so a dropped connection does not restart from zero; report progress; and survive an app relaunch by reconnecting to the in-flight transfer. Explain the session configuration, why you upload from a file rather than an in-memory Data, how the system wakes your app on completion, and how you reconcile progress and completion after a cold launch.
Design an upload for a 500 MB video that must finish even if the user backgrounds the app or it is evicted. Requirements: use a background URLSession so the transfer continues while the app is suspended; support a resumable or multipart/chunked upload so a dropped connection does not restart from zero; report progress; and survive an app relaunch by reconnecting to the in-flight transfer. Explain the session configuration, why you upload from a file rather than an in-memory Data, how the system wakes your app on completion, and how you reconcile progress and completion after a cold launch.
Use a background URLSession and a file-backed upload task, not in-memory Data, so the OS owns the transfer while the app is suspended. A resumable or chunked protocol lets a dropped connection resume mid-file. On completion the system relaunches the app and calls your delegate; recreate the session with the same identifier to reconnect.
Common mistakes
- ✗Uploading from in-memory
Data, which a suspended app cannot keep alive - ✗Using a
defaultsession that dies when the app is suspended - ✗Restarting the whole file on any connection drop instead of resuming
Follow-up questions
- →What does
handleEventsForBackgroundURLSessiondo after a relaunch? - →How does a server support resuming a partially received upload?
SeniorDesignOccasionalDesign connectivity handling for an app that lets users create and edit records while offline. Requirements: observe reachability with NWPathMonitor, distinguishing WiFi, cellular, and constrained or expensive paths; queue pending mutations durably while offline so nothing is lost on a crash; and when the network returns, drain the queue in order, retrying failures with backoff and reconciling conflicts with the server's copy. Explain how you detect the transition to online, why you persist the queue rather than hold it in memory, how you order and deduplicate mutations, and how you resolve a write the server has since changed.
Design connectivity handling for an app that lets users create and edit records while offline. Requirements: observe reachability with NWPathMonitor, distinguishing WiFi, cellular, and constrained or expensive paths; queue pending mutations durably while offline so nothing is lost on a crash; and when the network returns, drain the queue in order, retrying failures with backoff and reconciling conflicts with the server's copy. Explain how you detect the transition to online, why you persist the queue rather than hold it in memory, how you order and deduplicate mutations, and how you resolve a write the server has since changed.
Observe paths with NWPathMonitor — online/offline and constrained — rather than pinging a host. Persist each pending mutation to a durable on-disk queue so a crash loses nothing. On a satisfied path, drain it in order, retrying failures with backoff and deduplicating by a client-assigned id. Resolve conflicts with a policy — last-write-wins, server-wins, or merge.
Common mistakes
- ✗Holding the pending queue only in memory, losing it on a crash
- ✗Pinging a server to detect connectivity instead of observing the path
- ✗Replaying mutations with no dedup, duplicating server-side writes
Follow-up questions
- →Why can a satisfied path still fail the very next request?
- →How does a client-assigned id give you idempotent replays?
SeniorDesignOccasionalYour team is choosing the API paradigm for a new iOS client and weighs three options — a REST/JSON API, a GraphQL API, and gRPC with protobuf. The app has list and detail screens that over-fetch on REST, a slow server release cadence, and a bandwidth-sensitive user base. Compare the three across payload size, over- and under-fetching, versioning and schema evolution, caching (HTTP and client), tooling and code generation, and streaming. When would you pick each for a mobile client, and what does each cost you operationally? Recommend one for this app and justify it.
Your team is choosing the API paradigm for a new iOS client and weighs three options — a REST/JSON API, a GraphQL API, and gRPC with protobuf. The app has list and detail screens that over-fetch on REST, a slow server release cadence, and a bandwidth-sensitive user base. Compare the three across payload size, over- and under-fetching, versioning and schema evolution, caching (HTTP and client), tooling and code generation, and streaming. When would you pick each for a mobile client, and what does each cost you operationally? Recommend one for this app and justify it.
REST is simple and HTTP-cacheable but tends to over-fetch and needs explicit versioning. GraphQL fetches exactly the needed fields, killing over/under-fetch and easing evolution, but HTTP caching gets harder. gRPC/protobuf gives the smallest payloads, codegen, and streaming, yet loses HTTP caching. Here I would pick GraphQL, or gRPC if payload size dominates.
Common mistakes
- ✗Claiming GraphQL is always faster, ignoring its weaker HTTP caching
- ✗Treating gRPC as a drop-in for cache-heavy REST endpoints
- ✗Picking a paradigm for hype rather than payload and evolution needs
Follow-up questions
- →How does each paradigm handle a breaking change to a shared field?
- →Which paradigm best fits a live, server-pushed feed and why?
SeniorDesignRareA networking SDK is shared by several feature modules in a large app. Design it so you can evolve it without breaking callers: keep the public API small and stable, version it with semantic versioning, and let each module add its own behavior — logging, auth, retries — through interceptors without forking the core. Explain how you separate the stable public surface from internal types, how an ordered interceptor chain lets modules extend request and response handling, how you deprecate and migrate an API without a breaking version bump, and how you stop one module's interceptor from affecting another module's traffic.
A networking SDK is shared by several feature modules in a large app. Design it so you can evolve it without breaking callers: keep the public API small and stable, version it with semantic versioning, and let each module add its own behavior — logging, auth, retries — through interceptors without forking the core. Explain how you separate the stable public surface from internal types, how an ordered interceptor chain lets modules extend request and response handling, how you deprecate and migrate an API without a breaking version bump, and how you stop one module's interceptor from affecting another module's traffic.
Expose a small public protocol surface and keep concrete types internal, so internals evolve freely. Version with semantic versioning — additive changes bump the minor, breaking ones the major — and deprecate via availability annotations before removal. Modules extend behavior through an ordered interceptor chain, scoped per client so one module never touches another's traffic.
Common mistakes
- ✗Exposing concrete internal types, so any refactor becomes a breaking change
- ✗Bumping the major version for additive, backward-compatible changes
- ✗Sharing one global interceptor list across all modules' clients
Follow-up questions
- →How do availability annotations let you deprecate an API without breaking builds?
- →Where in the chain should an auth interceptor sit relative to logging?