Идемпотентность API на собеседовании системного аналитика
Карьерник — 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"}Серверная сторона:
- При первом запросе с этим ключом — выполнить операцию, сохранить результат с TTL (24-72 часа стандартно)
- При повторном с тем же ключом — вернуть тот же результат, не выполняя ещё раз
- Если тот же ключ с другим телом — 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.
Дубли в платежах
Самый частый кейс: клиент инициировал платёж, получил timeout, повторяет — без идемпотентности система списывает деньги дважды.
Защита:
- Клиент генерирует UUID до запроса
- Включает в
Idempotency-Key - На retry использует тот же ключ
- Сервер дедуплицирует
На стороне сервера БД:
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. Без транзакционной защиты два параллельных запроса с одним ключом могут оба пройти.
Связанные темы
- HTTP методы и коды на собесе SA
- REST API на собесе SA
- Webhook, polling, SSE и WebSocket
- Подготовка к собесу системного аналитика
- Идемпотентность пайплайна для DE
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+ вопросами для собесов.