Идемпотентность API на собеседовании системного аналитика

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Карьерник — Duolingo для аналитиков: 10 минут в день тренируй SQL, Python, A/B, статистику, метрики и ещё 3 темы собеса. 1500+ вопросов в Telegram-боте. Бесплатно.

Зачем идемпотентность спрашивают

В любой интеграции рано или поздно сеть рвётся. Клиент отправил POST /payments, не получил ответ — повторяет. Без идемпотентности это два списания вместо одного. Системный аналитик пишет ТЗ так, чтобы такой кейс не превратился в продакшн-инцидент.

Главная боль без понимания темы — ТЗ описывает «успешный сценарий», что делать при таймауте сети — не сказано. Команда внедряет «ретраи 3 раза», получает дубли, бухгалтерия выгребает.

HTTP-методы и идемпотентность

Метод Идемпотентный
GET Да
HEAD Да
OPTIONS Да
PUT Да
DELETE Да
PATCH Зависит от тела
POST Нет

Идемпотентный — повторный вызов с тем же телом даёт тот же результат.

  • PUT /users/123 {name: "Anna"} — сколько ни вызови, конечное состояние одно.
  • DELETE /users/123 — один раз удалили, повтор → 404, но конечное состояние то же.
  • POST /payments {amount: 1000} — каждый вызов создаёт новый платёж.

Idempotency-Key

Чтобы сделать POST идемпотентным — заголовок Idempotency-Key (UUID, генерируется клиентом).

POST /payments
Idempotency-Key: 8a1b2c3d-f4e5-...
Content-Type: application/json

{"amount": 1000, "to": "user-123"}

Серверная сторона:

  1. При первом запросе с этим ключом — выполнить операцию, сохранить результат с TTL (24-72 часа стандартно)
  2. При повторном с тем же ключом — вернуть тот же результат, не выполняя ещё раз
  3. Если тот же ключ с другим телом — 422 Unprocessable Entity (защита от ошибок клиента)

Свойства:

  • TTL ключа должен быть больше maximum retry timeout
  • Ключ привязан к клиенту (один клиент не может «подобрать» чужой ключ)
  • На стороне сервера — Redis/Postgres с (key, response, status_code, expires_at)

Стандарт в Stripe API, обязателен для платежей.

Retry-политика

Минимальная политика для production:

  • Ретраить: 5xx, 408, 429
  • Не ретраить: 4xx (кроме указанных) — баг в запросе, повтор не поможет
  • Backoff: exponential (1s, 2s, 4s, 8s, ...) с jitter
  • Max attempts: 3-5
  • Timeout: explicit на каждый attempt

При исчерпании попыток — переводить запрос в dead-letter queue для ручного разбора.

Server-side rate limiting:

HTTP/1.1 429 Too Many Requests
Retry-After: 30

Клиент должен уважать Retry-After.

Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Дубли в платежах

Самый частый кейс: клиент инициировал платёж, получил timeout, повторяет — без идемпотентности система списывает деньги дважды.

Защита:

  1. Клиент генерирует UUID до запроса
  2. Включает в Idempotency-Key
  3. На retry использует тот же ключ
  4. Сервер дедуплицирует

На стороне сервера БД:

CREATE TABLE idempotency_keys (
    key VARCHAR PRIMARY KEY,
    client_id BIGINT,
    request_hash VARCHAR,
    response_body JSONB,
    response_status INT,
    expires_at TIMESTAMP
);

Запрос → проверить ключ → если есть и тело совпадает — отдать сохранённый ответ. Если есть и тело отличается — 422. Если нет — выполнить, сохранить.

Тонкость: транзакция должна включать сохранение ключа. Иначе race condition: два параллельных запроса с одним ключом могут оба пройти.

Как описать в ТЗ

Минимум для критичной операции:

  • Заголовок Idempotency-Key: обязательный, UUID
  • TTL: 24 часа (или дольше)
  • Поведение при повторе: «возвращается тот же ответ»
  • Поведение при конфликте тела: 422 + error: "idempotency_key_mismatch"
  • Retry-policy на стороне клиента: ретраить только 5xx, 408, 429 с backoff

Пример акцептов:

Сценарий: повторный запрос с тем же ключом
Given сделан успешный запрос с Idempotency-Key=K1
When клиент повторяет тот же запрос с Idempotency-Key=K1 и тем же телом
Then API возвращает 200 + body первого ответа
And новой записи в БД не создаётся

Частые ошибки

Idempotency-Key не описан в ТЗ. Команда не реализует, дубли в проде через месяц.

TTL короткий. Сетевые проблемы могут длиться часами. TTL 24-72 часа.

Не проверять request_hash. Без проверки тела можно отдать чужой ответ или allow подмену тела при том же ключе.

Хранить ответы в памяти процесса. При рестарте сервиса дедупликация ломается. Нужно persistent storage.

Использовать ID запроса как key. ID должен быть детерминирован на клиенте. Если ID генерится сервером — клиент не может его прислать.

Применять идемпотентность к GET. GET идемпотентен по природе. Idempotency-Key не нужен.

Игнорировать race condition. Без транзакционной защиты два параллельных запроса с одним ключом могут оба пройти.

Связанные темы

FAQ

Можно ли использовать timestamp как Idempotency-Key?

Не рекомендуется. Два запроса в одну миллисекунду получат тот же ключ, что вызовет ложные конфликты. UUID или request_id, который точно уникален.

Что делать с retry при 422?

Не ретраить. 422 — постоянная ошибка тела или ключа. Retry даст тот же результат.

Идемпотентность нужна для каждого endpoint?

Нет. Для критичных операций (платежи, отправка SMS, заказы). Для GET и idempotent методов — не нужна. Для второстепенных POST — по необходимости.

Можно ли реализовать идемпотентность через UNIQUE constraint в БД?

Да. UNIQUE на (client_id, idempotency_key) в операциях. Дубль → ошибка → API ловит и отдаёт уже существующий результат. Проще, чем отдельная таблица ключей.

Что если клиент потерял Idempotency-Key и не знает результат?

Клиент должен сохранить ключ до получения окончательного ответа. Если потерял — иначе не сможет повторить безопасно. На стороне UX — local storage / БД клиента.

Это официальная информация?

Нет. Статья основана на RFC drafts (Idempotency-Key), документации Stripe API и общей практике интеграций.


Тренируйте системный анализ — откройте тренажёр с 1500+ вопросами для собесов.