> Как избежать двойного оформления заказа при нестабильном интернете и повторных запросах? (iOS, Swift)

Уровень: senior · Роль: mobile · Категория: Технические вопросы

Компании: Travelata

Стек: iOS, Swift

> Пример ответа

Короткий ответ

Идемпотентность - ключевой принцип: клиент генерирует уникальный idempotencyKey для каждого заказа и передаёт его в запросе. Сервер хранит ключ и результат обработки, при повторном запросе с тем же ключом возвращает сохранённый ответ, не создавая новый заказ. На iOS дополнительно нужна очередь запросов с retry-логикой и блокировкой UI до подтверждения успеха или окончательного failure. Это исключает дубли при таймаутах, повторных тапах и восстановлении сети.

Подробное объяснение

Проблема возникает из-за того, что HTTP не гарантирует доставку ответа: клиент может не получить подтверждение, хотя сервер уже создал заказ. Повторный запрос без идемпотентности создаёт второй заказ.

Решение на уровне API:

  • Сервер принимает Idempotency-Key в заголовке (или в теле).
  • Хранит пару key → response в кэше (Redis, БД) с TTL.
  • При повторном запросе с тем же ключом возвращает закешированный ответ, не выполняя бизнес-логику.

Решение на уровне iOS:

  • Генерация UUID перед отправкой (один раз на операцию).
  • Хранение pending-операций в памяти или в UserDefaults (для переживания рестарта).
  • Retry с exponential backoff при network errors, но не при бизнес-ошибках (4xx).
  • Отключение кнопки "Оформить" до получения финального статуса.

Важно различать:

  • Таймаут - неизвестность результата, нужен retry с тем же ключом.
  • Ошибка валидации - повторять бессмысленно, нужно показать ошибку.
  • Успех - можно очистить ключ.

На практике

  1. Создаём OrderRequest с полем idempotencyKey = UUID().uuidString.
  2. Используем URLSession с кастомным URLCache для ответов на POST (нестандартно, но возможно).
  3. Оборачиваем запрос в AsyncOperation или используем Task с retry-циклом.
  4. При URLError.timedOut или networkConnectionLost - повторяем до 3 раз с задержкой 1s, 2s, 4s.
  5. При получении ответа - сохраняем ключ в completedKeys и не отправляем повторно.
  6. Если приложение убито во время запроса - при старте проверяем pendingOrders, если есть незавершённый - отправляем повторно с тем же ключом.

Пример кода

SWIFT
struct OrderRequest: Encodable {
let items: [CartItem]
let idempotencyKey: String
}
final class OrderService {
private let session: URLSession
private var pendingKeys = Set<String>()
private let defaults = UserDefaults.standard
func placeOrder(_ items: [CartItem]) async throws -> Order {
let key = UUID().uuidString
pendingKeys.insert(key)
var request = URLRequest(url: orderURL)
request.httpMethod = "POST"
request.setValue(key, forHTTPHeaderField: "Idempotency-Key")
request.httpBody = try JSONEncoder().encode(OrderRequest(items: items, idempotencyKey: key))
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw OrderError.serverError
}
pendingKeys.remove(key)
defaults.set(key, forKey: "completedOrderKey")
return try JSONDecoder().decode(Order.self, from: data)
} catch {
// retry logic with same key
for delay in [1.0, 2.0, 4.0] {
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
do {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { continue }
pendingKeys.remove(key)
return try JSONDecoder().decode(Order.self, from: data)
} catch { continue }
}
throw error
}
}
}

Как отвечать на собеседовании

Начни с идемпотентности как основного механизма. Затем опиши, как клиент должен генерировать и хранить ключ. Упомяни, что retry - это отдельная задача, и нельзя просто повторять запрос без ключа. Покажи понимание разницы между сетевыми ошибками (retry) и бизнес-ошибками (no retry). Добавь про обработку сценария убитого приложения. Если спросят про гонки - объясни, что сервер должен атомарно проверять ключ и создавать заказ (unique constraint на ключ).

Что проверяет интервьюер

  • Понимание идемпотентности и её реализации на сервере.
  • Умение проектировать надёжный клиент с retry-логикой.
  • Знание жизненного цикла запроса в iOS (URLSession, Task, cancellation).
  • Умение обрабатывать edge cases: таймаут, потеря сети, рестарт приложения.
  • Способность объяснить trade-off между повторными запросами и нагрузкой на сервер.

Типичные ошибки

  • Повторная отправка без idempotencyKey - дубликаты заказов.
  • Retry на все ошибки, включая 4xx - бесконечные попытки с невалидными данными.
  • Генерация нового ключа при каждом retry - теряется смысл идемпотентности.
  • Игнорирование сценария, когда ответ потерян, но заказ создан - клиент показывает ошибку, а заказ есть.
  • Блокировка UI навсегда при отсутствии сети - нужен таймаут и возможность отмены.
  • Хранение ключа только в памяти - после рестарта приложения ключ теряется, повторная отправка создаст дубль.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью