> Как избежать двойного оформления заказа при нестабильном интернете и повторных запросах? (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 с тем же ключом.
- Ошибка валидации - повторять бессмысленно, нужно показать ошибку.
- Успех - можно очистить ключ.
На практике
- Создаём
OrderRequestс полемidempotencyKey = UUID().uuidString. - Используем
URLSessionс кастомнымURLCacheдля ответов на POST (нестандартно, но возможно). - Оборачиваем запрос в
AsyncOperationили используемTaskсretry-циклом. - При
URLError.timedOutилиnetworkConnectionLost- повторяем до 3 раз с задержкой 1s, 2s, 4s. - При получении ответа - сохраняем ключ в
completedKeysи не отправляем повторно. - Если приложение убито во время запроса - при старте проверяем
pendingOrders, если есть незавершённый - отправляем повторно с тем же ключом.
Пример кода
SWIFTstruct OrderRequest: Encodable {let items: [CartItem]let idempotencyKey: String}final class OrderService {private let session: URLSessionprivate var pendingKeys = Set<String>()private let defaults = UserDefaults.standardfunc placeOrder(_ items: [CartItem]) async throws -> Order {let key = UUID().uuidStringpendingKeys.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 keyfor 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 навсегда при отсутствии сети - нужен таймаут и возможность отмены.
- Хранение ключа только в памяти - после рестарта приложения ключ теряется, повторная отправка создаст дубль.
> Похожие задачи по mobile
В чем разница HTTP методов GET, POST, PUT, DELETE и когда их использовать
Как устроены слои в чистой архитектуре?
Какие есть варианты кэширования на уровне приложения?
Расскажите о своем предыдущем опыте
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью