APIs & Data Management

Chapter 5
Reliable Networking Architecture

Certificate in IOS Development
APIs & Data Management   •   Certificate

Reliable Networking Architecture

Introduction

An iOS application is only as reliable as the network operations on which its user experience depends. Mobile networks change frequently: a device may move between Wi‑Fi and cellular data, lose connectivity, encounter slow servers, receive expired credentials, or be asked to repeat a request after a temporary failure. A networking layer that merely sends HTTP requests is therefore insufficient. A production-quality client must make failure understandable, cancellation safe, testing practical, and behavior consistent across the application.

This chapter presents a reusable architecture for API clients in Swift. It focuses on protocols, dependency injection, structured concurrency, cancellation, timeouts, retries, pagination, rate-limit handling, and consistent error models. The examples use URLSession and Swift concurrency, but the design principles apply to most REST and JSON APIs.

Learning Objectives

By the end of this chapter, you should be able to:

  1. Explain the responsibilities of a reliable networking layer.
  2. Define API clients using protocols and inject their dependencies.
  3. Implement asynchronous requests using structured concurrency.
  4. Distinguish cancellation, timeout, transport, HTTP, decoding, and domain errors.
  5. Apply safe retry policies and explain why not every request should be retried.
  6. Implement cursor-based and page-based pagination.
  7. Interpret rate-limit responses and use server-provided timing information.
  8. Design and test a networking component independently of a live server.

1. The Role of a Networking Architecture

A networking architecture is the set of abstractions and rules that govern how an application communicates with remote services. Its purpose is not simply to hide URLSession; it should provide a stable boundary between application features and changing transport details.

A well-designed networking layer usually contains the following responsibilities:

  • constructing valid requests;
  • attaching authentication and common headers;
  • performing asynchronous transport;
  • validating HTTP status codes;
  • decoding successful responses;
  • translating failures into a consistent error model;
  • applying cancellation, timeout, retry, and rate-limit policies;
  • supporting pagination;
  • enabling deterministic tests.

It should not contain presentation logic, such as deciding which alert a view should display. The networking layer reports facts and typed failures; higher layers decide how those facts affect the user interface.

A useful separation is:

LayerPrimary responsibility
View or view modelUser interaction and presentation state
Repository or serviceFeature-oriented operations and caching decisions
API clientEndpoint operations and response decoding
HTTP transportRequest execution and protocol mechanics
ServerAuthoritative remote data and business rules

This separation reduces coupling. For example, changing from a real server to a mock server should not require rewriting a view model.

2. HTTP Concepts Relevant to Reliability

An HTTP request normally includes a method, URL, headers, and sometimes a body. Common methods have different semantic expectations:

  • GET retrieves a representation and should not change server state.
  • POST commonly creates a resource or triggers an operation. Repeating it may create duplicates.
  • PUT replaces a resource and is generally intended to be idempotent.
  • PATCH partially updates a resource; its retry safety depends on the API contract.
  • DELETE removes a resource and is often idempotent, although the first and subsequent responses may differ.

An operation is idempotent when repeating it produces the same intended server state as performing it once. Idempotency is a semantic property, not merely a property of an HTTP verb. A POST endpoint can be made safely retryable when it accepts an idempotency key and guarantees that repeated keys represent the same operation.

Status codes should be interpreted systematically:

Status range or codeMeaning for a client
2xxRequest succeeded; decode the response if one is expected
3xxRedirection; normally handled by the transport, subject to policy
400Invalid request or parameters
401Missing or invalid authentication
403Authenticated but not permitted
404Resource or endpoint not found
409Conflict with current server state
429Rate limit exceeded
5xxServer-side failure; sometimes transient

The status code alone is not always enough. An API may return a structured error body containing a machine-readable code, message, request identifier, or validation fields. A reliable client preserves useful information without exposing sensitive server details directly to users.

3. Protocol-Oriented API Clients

A protocol defines capabilities without committing callers to a concrete implementation. The application can depend on an APIClient protocol while production code uses a network implementation and tests use a stub.

protocol APIClient {
    func request<T: Decodable>(_ endpoint: Endpoint<T>) async throws -> T
}

The endpoint describes request-specific information:

enum HTTPMethod: String {
    case GET, POST, PUT, PATCH, DELETE
}

struct Endpoint<Response: Decodable> {
    let path: String
    let method: HTTPMethod
    let query: [URLQueryItem]
    let headers: [String: String]
    let body: Data?
}

The generic response type connects an endpoint to its expected decoded representation. This prevents every caller from repeating decoding code and makes an incorrect response type visible at compile time.

A practical implementation can use a lower-level transport protocol:

protocol HTTPTransport {
    func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse)
}

struct URLSessionTransport: HTTPTransport {
    let session: URLSession

    func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
        let (data, response) = try await session.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse else {
            throw NetworkError.invalidResponse
        }
        return (data, httpResponse)
    }
}

This division has two benefits. First, request policy and response interpretation are separated from the system transport. Second, a test can supply a fake HTTPTransport that returns predetermined data or errors.

4. Dependency Injection

Dependency injection means providing an object with the collaborators it needs instead of constructing those collaborators internally. Constructor injection is usually the clearest form:

struct DefaultAPIClient: APIClient {
    let baseURL: URL
    let transport: HTTPTransport
    let decoder: JSONDecoder

    init(baseURL: URL,
         transport: HTTPTransport,
         decoder: JSONDecoder = JSONDecoder()) {
        self.baseURL = baseURL
        self.transport = transport
        self.decoder = decoder
    }

    func request<T: Decodable>(_ endpoint: Endpoint<T>) async throws -> T {
        var components = URLComponents(url: baseURL.appendingPathComponent(endpoint.path),
                                       resolvingAgainstBaseURL: false)
        components?.queryItems = endpoint.query
        guard let url = components?.url else {
            throw NetworkError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = endpoint.method.rawValue
        request.httpBody = endpoint.body
        endpoint.headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }

        let (data, response) = try await transport.send(request)
        try validate(response: response, data: data)
        do {
            return try decoder.decode(T.self, from: data)
        } catch {
            throw NetworkError.decoding(underlying: error)
        }
    }

    private func validate(response: HTTPURLResponse, data: Data) throws {
        guard 200..<300 ~= response.statusCode else {
            throw NetworkError.http(statusCode: response.statusCode,
                                    body: data,
                                    retryAfter: response.value(forHTTPHeaderField: "Retry-After"))
        }
    }
}

In production, dependencies may include an authentication provider, clock, retry policy, logger, and persistence store. Injecting a clock is particularly useful when testing delays and expiration logic because tests should not have to wait in real time.

5. A Consistent Error Model

Without a consistent error model, feature code tends to inspect unrelated errors such as URLError, JSON decoder errors, and arbitrary server messages. A typed model makes recovery decisions explicit.

enum NetworkError: Error {
    case invalidURL
    case invalidResponse
    case transport(underlying: Error)
    case cancelled
    case timeout
    case http(statusCode: Int, body: Data, retryAfter: String?)
    case decoding(underlying: Error)
    case unauthorized
    case rateLimited(retryAfter: Duration?)
}

The exact cases should reflect application needs. A useful classification is:

  1. Cancellation: the caller no longer wants the result.
  2. Timeout: the operation exceeded a configured time limit.
  3. Transport failure: DNS failure, lost connection, or inability to reach the server.
  4. HTTP failure: the server responded with a non-success status.
  5. Decoding failure: bytes were received, but they did not match the expected schema.
  6. Domain failure: the server response is valid but violates an application rule.

Cancellation is not an ordinary failure. If a user leaves a screen, the application should normally stop work silently rather than show an error alert. Likewise, a 401 may require refreshing credentials, while a 422 validation response should usually be shown as a form error rather than retried.

The original error should be retained for diagnostics, but logs must avoid access tokens, passwords, private user data, and unnecessary response bodies.

6. Structured Concurrency and Cancellation

Swift structured concurrency represents asynchronous work as a hierarchy. A parent task owns child tasks, and cancellation propagates down that hierarchy. This is preferable to unmanaged completion handlers because the lifetime of work is connected to the lifetime of the operation that requested it.

For example, a view model can cancel an in-flight search when a new query arrives:

@MainActor
final class SearchViewModel: ObservableObject {
    @Published private(set) var results: [SearchItem] = []
    private var searchTask: Task<Void, Never>?
    let client: APIClient

    init(client: APIClient) {
        self.client = client
    }

    func search(_ text: String) {
        searchTask?.cancel()
        searchTask = Task {
            do {
                try await Task.sleep(for: .milliseconds(250))
                try Task.checkCancellation()
                let endpoint = SearchEndpoint.make(query: text)
                results = try await client.request(endpoint)
            } catch is CancellationError {
                // Expected when a newer query replaces this task.
            } catch {
                // Convert the error into presentation state.
            }
        }
    }
}

The delay provides debouncing, but it should not be confused with a network timeout. A timeout limits how long an operation may wait; debouncing delays the start of an operation to avoid unnecessary requests.

A networking implementation should preserve cancellation. It should not catch every error and convert it into a generic failure. In particular, cancellation should be rethrown or mapped deliberately. A retry loop must also check cancellation before sleeping and before starting another attempt.

7. Timeouts

A timeout is a policy boundary. It limits waiting, but it cannot guarantee that the server stopped processing a request. This distinction matters for state-changing operations: a client may time out even though the server completed the operation.

A timeout can be implemented with a task group that races the operation against a sleep task. Conceptually:

func withTimeout<T: Sendable>(
    _ duration: Duration,
    operation: @escaping @Sendable () async throws -> T
) async throws -> T {
    try await withThrowingTaskGroup(of: T.self) { group in
        group.addTask {
            try await operation()
        }
        group.addTask {
            try await Task.sleep(for: duration)
            throw NetworkError.timeout
        }

        defer { group.cancelAll() }
        guard let result = try await group.next() else {
            throw NetworkError.timeout
        }
        return result
    }
}

This pattern assumes that the operation cooperates with cancellation. A timeout value should reflect the endpoint and user experience. Very short limits create avoidable failures; very long limits make the interface appear frozen. Connection, resource, and request-level timeouts may also be configured through URLSessionConfiguration, but application-level policies are still useful for bounding composed operations.

8. Retry Policies

Retries can improve reliability for transient failures, but indiscriminate retries amplify server load and can duplicate state changes. A retry policy should answer four questions:

  • Is the failure likely to be temporary?
  • Is the request safe to repeat?
  • How many attempts are allowed?
  • How long should the client wait before trying again?

Usually retry candidates include transient transport errors, selected 5xx responses, and 429 responses. Authentication failures, malformed requests, decoding failures, and most 4xx responses should not be retried automatically.

A common backoff model is exponential backoff with jitter:

Dn = min(Dmax, D0 × 2n) + J

Here, D_n is the delay before retry number n, D_0 is the initial delay, D_max is the maximum backoff cap, and J is a random jitter value selected within an agreed range. The formula assumes that n = 0 denotes the first retry delay and that delays are measured in the same unit, such as seconds. Jitter prevents many clients that failed simultaneously from retrying at exactly the same moment. A production policy should also impose a maximum total retry time or attempt count.

For example, with D_0 = 0.5 s, D_max = 8 s, and no jitter, the first four delays are 0.5 s, 1 s, 2 s, and 4 s. The fifth would be 8 s because of the cap. These values are policy choices, not universal constants.

An illustrative retry wrapper is:

func retrying<T>(
    attempts: Int,
    operation: @escaping () async throws -> T
) async throws -> T {
    var attempt = 0
    while true {
        do {
            return try await operation()
        } catch is CancellationError {
            throw CancellationError()
        } catch {
            guard attempt < attempts - 1,
                  isTransient(error) else { throw error }
            let delay = min(8.0, 0.5 * pow(2.0, Double(attempt)))
            try await Task.sleep(for: .milliseconds(Int(delay * 1000)))
            attempt += 1
        }
    }
}

This simplified example omits jitter and server-provided retry timing. In a complete implementation, Retry-After should normally take precedence for rate-limit responses, subject to a client safety cap.

9. Rate Limits

A rate limit protects a service from excessive traffic. A server may communicate a limit through headers such as Retry-After, X-RateLimit-Remaining, or X-RateLimit-Reset; names and formats vary by API.

When the server returns 429 Too Many Requests, the client should:

  1. stop issuing immediate additional requests;
  2. parse Retry-After if present;
  3. wait for the specified duration or reset time;
  4. respect cancellation during the wait;
  5. avoid creating a retry storm across multiple screens or tasks.

Retry-After may be expressed as seconds or as an HTTP date, so parsing must follow the API specification. If the header is absent or invalid, a bounded exponential backoff is safer than a tight loop.

Rate-limit handling is also an architectural concern. A shared rate-limit coordinator can serialize or delay requests across features. It should not conceal permanent failures or wait indefinitely. User-facing behavior may include showing that an operation is temporarily unavailable, while background synchronization may defer work until a later opportunity.

10. Pagination

Large collections are commonly divided into pages. Two principal designs are offset pagination and cursor pagination.

Offset pagination uses a page number or offset and a page size, such as page=3&limit=20. It is simple and supports jumping to a page, but insertions and deletions can cause records to move between pages, producing duplicates or omissions during a changing query.

Cursor pagination returns a token representing the position in a stable ordering. The next request supplies that token. Cursor pagination is usually more robust for frequently changing collections, but cursors are opaque and generally should not be modified or assumed to be permanent.

A cursor response might be modeled as:

struct Page<Item: Decodable>: Decodable {
    let items: [Item]
    let nextCursor: String?
}

An asynchronous sequence can expose pages to callers without forcing the entire collection into memory:

struct Paginator<Item: Decodable>: AsyncSequence {
    typealias Element = Item
    let load: (_ cursor: String?) async throws -> Page<Item>

    struct AsyncIterator: AsyncIteratorProtocol {
        var cursor: String?
        var buffer: [Item] = []
        var finished = false
        let load: (_ cursor: String?) async throws -> Page<Item>

        mutating func next() async throws -> Item? {
            while buffer.isEmpty && !finished {
                let page = try await load(cursor)
                buffer = page.items
                cursor = page.nextCursor
                finished = cursor == nil
            }
            return buffer.isEmpty ? nil : buffer.removeFirst()
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(cursor: nil, load: load)
    }
}

The iterator must define what happens when a page is empty but still supplies a cursor. Some APIs permit this, so termination should be based on the documented cursor contract rather than only on item count. Production implementations may use an index instead of removeFirst() for efficiency with large pages.

Pagination also interacts with cancellation and retries. If a user leaves a screen, the next-page request should stop. A retry must not accidentally advance the cursor twice. The cursor should be updated only after a page is successfully received and decoded.

11. Authentication and Token Refresh

Authentication credentials should be managed separately from endpoint construction. An AuthProvider can supply a valid access token and refresh it when appropriate:

protocol AuthProvider: Sendable {
    func validAccessToken() async throws -> String
}

When a request receives 401, the client may attempt one controlled token refresh and then repeat the original request. It should not refresh repeatedly in an infinite loop. Concurrent requests also require coordination so that ten simultaneous 401 responses do not trigger ten refresh operations. An actor or single-flight mechanism can ensure that only one refresh is active while other requests await its result.

Tokens must be stored using appropriate platform security mechanisms, such as Keychain services. They should never be placed in URLs, logs, analytics events, or crash messages.

12. Testing the Networking Layer

Testing should cover behavior, not implementation details. Useful test cases include:

  • a successful response decodes into the expected model;
  • malformed JSON produces a decoding error;
  • a 404 is classified as an HTTP error;
  • cancellation prevents later retry attempts;
  • a transient error retries only within the configured limit;
  • a non-idempotent operation is not automatically repeated;
  • a 429 honors the server’s retry instruction;
  • pagination stops at the correct cursor;
  • an expired token triggers at most one refresh cycle.

A fake transport makes these tests fast and deterministic:

struct StubTransport: HTTPTransport {
    let result: Result<(Data, HTTPURLResponse), Error>

    func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) {
        try result.get()
    }
}

Tests should also verify request construction: HTTP method, path, query encoding, headers, body, and absence of secrets in logs. Injecting a clock or sleeper abstraction allows retry timing to be tested without real delays.

13. Illustrative Case: A News Reader Application

Background

Consider a fictional news reader application that displays personalized articles. It supports pull-to-refresh, infinite scrolling, search, and offline-friendly behavior. The application receives articles from a cursor-based API and is subject to a service limit of 60 requests per minute.

Problem

The initial implementation places URLSession calls directly in view controllers. Users report duplicate articles during scrolling, search results from old queries replacing newer results, and repeated error alerts when the network is unavailable. The server also reports bursts of requests after temporary failures.

Analysis

The problems have different causes:

  • direct transport calls prevent consistent policy enforcement;
  • pagination state is updated before successful decoding;
  • search tasks are not cancelled when the query changes;
  • all errors are presented identically;
  • immediate retries create a retry storm;
  • no shared rate-limit policy coordinates requests.

Approach

The team introduces an APIClient protocol and injects a production client into repositories and a stub client into tests. Search uses a cancellable task with a short debounce. Article loading uses cursor pagination, updating the cursor only after a successful page response. The client maps failures into NetworkError and distinguishes cancellation from user-visible failures.

GET requests may be retried for selected transient errors using capped exponential backoff and jitter. The client honors Retry-After for 429 responses. A shared coordinator prevents immediate requests when the service reports that the limit has been reached. The view model displays a non-blocking offline state for transport failures and a specific sign-in action for authentication failures.

Outcome

The application no longer shows stale search results because cancelled tasks do not publish their results. Scrolling does not skip pages because cursor advancement is transactional from the client’s perspective: it occurs only after successful decoding. Server load decreases because retries are bounded and coordinated. Tests can reproduce failures without depending on network availability.

Learning Points

Reliability is achieved by combining small policies rather than by a single networking class. Cancellation protects user intent, typed errors support appropriate recovery, pagination preserves collection correctness, and dependency injection makes all of these behaviors testable.

14. Limitations, Governance, and Best Practices

No client can guarantee network availability or server correctness. A timeout may occur after a server-side state change, and a successful HTTP response may contain stale business data. Applications should design state-changing operations with idempotency keys where supported and should provide reconciliation mechanisms for uncertain outcomes.

Privacy and security are also part of reliability. Use HTTPS, validate certificates through platform mechanisms, minimize transmitted personal data, and define retention and logging policies. Avoid logging full URLs when query parameters may contain sensitive information. Authentication failures should not reveal whether protected resources exist.

A practical checklist is:

  • define endpoint contracts and response models;
  • centralize request construction;
  • use protocols at application boundaries;
  • inject transport, authentication, clock, and policy dependencies;
  • preserve cancellation;
  • set bounded timeouts;
  • retry only transient and semantically safe operations;
  • honor server rate-limit instructions;
  • make pagination state explicit;
  • classify errors consistently;
  • test success, failure, cancellation, and concurrency paths;
  • instrument latency and failure categories without collecting unnecessary personal data.

Contemporary iOS development increasingly uses Swift concurrency, actors, typed networking abstractions, and observation-driven interfaces. These tools improve clarity, but they do not replace protocol understanding or sound failure policy. A modern implementation should also consider connectivity awareness, background execution limits, server schema evolution, and accessibility implications of delayed or failed content.

Chapter Summary

Reliable networking architecture creates a stable boundary between an iOS application and remote services. Protocol-oriented clients and dependency injection separate application behavior from transport implementation and make testing possible. Structured concurrency gives asynchronous work a clear lifetime, while cancellation prevents obsolete operations from consuming resources or publishing stale results.

A consistent error model distinguishes cancellation, timeout, transport, HTTP, decoding, and domain failures. Timeouts bound waiting but do not prove that a server stopped processing a request. Retries are useful only for appropriate transient failures and must consider idempotency, bounded attempts, exponential backoff, jitter, and server-provided rate-limit instructions. Pagination requires explicit cursor or offset state and careful interaction with cancellation and retries. Authentication refresh, secure token storage, privacy-conscious logging, and deterministic tests complete the architecture. Reliability is therefore a set of coordinated design decisions rather than a single API call.

Key Terms

Protocol: A Swift declaration describing required behavior without prescribing a concrete implementation.

Dependency injection: Supplying an object’s collaborators from outside that object.

Structured concurrency: A concurrency model in which asynchronous tasks form a managed parent–child hierarchy.

Cancellation: A signal that an asynchronous operation should stop because its result is no longer wanted.

Timeout: A limit on how long an operation may wait before being treated as unsuccessful.

Transport error: A failure to communicate, such as DNS failure or a lost connection.

HTTP error: A response with a non-success HTTP status code.

Decoding error: A failure to convert response data into the expected model.

Idempotency: The property that repeating an operation produces the same intended server state.

Retry policy: Rules determining whether, when, and how an operation is attempted again.

Exponential backoff: A retry delay that grows approximately by powers of two until capped.

Jitter: Random variation added to a retry delay to reduce synchronized retries.

Rate limit: A server-imposed restriction on request frequency or volume.

Retry-After: An HTTP header that can tell a client when to retry.

Pagination: Dividing a collection into multiple responses.

Cursor pagination: Pagination using an opaque position token supplied by the server.

Offset pagination: Pagination using a page number or numeric offset.

Single-flight refresh: Coordination ensuring that concurrent requests share one credential-refresh operation.

Structured error model: A defined set of error categories used consistently throughout an application.

Review Questions

Short Answer Questions

  1. Why should an API client be defined behind a protocol?
  2. How does cancellation differ from a timeout?
  3. Name three categories of errors a networking layer should distinguish.
  4. Why can retrying a POST request be dangerous?
  5. What information can a Retry-After header provide?

Long Answer Questions

  1. Explain how dependency injection improves the testability and maintainability of an iOS networking layer.
  2. Describe a safe retry policy, including transient failures, idempotency, exponential backoff, jitter, and attempt limits.
  3. Compare cursor pagination with offset pagination and identify an appropriate use case for each.
  4. Explain how structured concurrency and cancellation prevent stale search results in an iOS application.
  5. Design a consistent error model for a client that communicates with an authenticated REST API.

Analytical / Application Questions

  1. A request receives 429 Too Many Requests with a Retry-After value of 20 seconds. What should the client do, and which safeguards should apply if several screens are making requests concurrently?
  2. A payment request times out, but the user does not know whether the server completed it. Analyze the risks of blindly retrying and propose a safer design.
  3. An infinite-scroll feed returns duplicate articles after new articles are published. Determine whether offset or cursor pagination is more likely to prevent this problem and explain the additional client-side safeguards required.

Multiple Choice Questions with Answers

Question 1

Which design most directly enables replacing a real network implementation with a test double?

A. Hard-coding URLSession in every view controller

B. Defining an APIClient protocol and injecting its implementation

C. Increasing the request timeout

D. Returning raw Data from every endpoint

Correct Answer: B. Defining an APIClient protocol and injecting its implementation

Question 2

Which failure should normally not be treated as an ordinary user-facing network error?

A. HTTP 404

B. Malformed JSON

C. Task cancellation

D. HTTP 503

Correct Answer: C. Task cancellation

Question 3

What is the main purpose of exponential backoff with jitter?

A. To guarantee a successful request

B. To reduce synchronized retry bursts

C. To encrypt response data

D. To increase page size

Correct Answer: B. To reduce synchronized retry bursts

Question 4

Which response most directly indicates that a client should consider rate-limit handling?

A. 200 OK

B. 201 Created

C. 304 Not Modified

D. 429 Too Many Requests

Correct Answer: D. 429 Too Many Requests

Question 5

Why is cursor pagination often preferable for a frequently changing feed?

A. It always permits jumping to any page number

B. It avoids all decoding requirements

C. It can provide a more stable position as records change

D. It guarantees that the server never deletes data

Correct Answer: C. It can provide a more stable position as records change

Question 6

What should generally happen to a pagination cursor after a page fails to decode?

A. Advance it immediately

B. Discard all future pages permanently

C. Update it only after successful receipt and decoding

D. Replace it with a random value

Correct Answer: C. Update it only after successful receipt and decoding

Question 7

Which operation is most clearly unsafe to retry automatically without an idempotency mechanism?

A. A read-only GET request

B. A POST that creates an order

C. A request for a static configuration file

D. A health-check request

Correct Answer: B. A POST that creates an order

Question 8

What does a timeout establish?

A. That the server definitely stopped processing

B. A bounded period the client will wait

C. That the response is correctly decoded

D. That the request is safe to repeat

Correct Answer: B. A bounded period the client will wait

Question 9

Which dependency is especially useful for testing retry delays without making tests wait in real time?

A. A real production database

B. An injected clock or sleeper

C. A larger JSON payload

D. A view controller outlet

Correct Answer: B. An injected clock or sleeper

Question 10

What is a major reason to coordinate concurrent token refresh operations?

A. To increase the number of expired tokens

B. To ensure every request uses a different password

C. To prevent many simultaneous 401 responses from triggering redundant refreshes

D. To disable HTTPS

Correct Answer: C. To prevent many simultaneous 401 responses from triggering redundant refreshes

APIs & Data Management
Chapter 5 of 14