Идемпотентность — это не про повтор запроса. Это контроль.

Классический пример — это оплата в приложении.

Клиент отправил запрос. Сервер его получил и списал деньги. Но ответ потерялся по дороге. Что знает приложение? Почти ничего. Оно видит ошибку сети и не может отличить два случая:

- запрос не дошёл до сервера; - запрос дошёл, операция выполнилась, но ответ не вернулся.

Именно поэтому обычный retry может быть опасен.

Перед первой попыткой клиент создаёт идентификатор бизнес-операции: let operationId = UUID() Важно: он создаётся один раз, а не перед каждым повторным запросом. Если сеть оборвалась, запрос можно отправить снова, но с тем же operationId.

Сервер проверяет: эта операция уже выполнялась?

Если нет — выполняет её и сохраняет результат. Если да — не проводит действие второй раз, а возвращает уже известный результат.

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

Но здесь начинается самое интересное. Просто хранить operationId недостаточно. Что если два одинаковых запроса придут одновременно?

Оба могут успеть проверить, что такой операции ещё нет, и оба начать её выполнять. Значит, защита должна быть атомарной. Например, через уникальное ограничение по operationId, чтобы только один запрос получил право создать операцию. Есть и более сложный случай.

Сервер: -выполнил платёж; -упал; -не успел сохранить результат.

После перезапуска приходит повторный запрос. Для нашей системы операция как будто неизвестна, хотя во внешней платёжной системе деньги уже могли быть списаны. Поэтому в серьёзной архитектуре идентификатор операции желательно протягивать как можно дальше по всей цепочке: наш клиент → наш сервер → внешний платёжный сервис. Иначе идемпотентность заканчивается на границе нашей системы.

На iOS есть ещё один важный момент. Если операция критичная, operationId нельзя держать только в памяти. Приложение может быть выгружено, пользователь откроет его снова, и если создать новый UUID — сервер воспримет это как новую операцию.

Поэтому незавершённую операцию часто стоит хранить локально: operationId payload state = awaitingResult

А после запуска не создавать новую, а сначала выяснить состояние старой. И вот здесь идемпотентность уже превращается не просто в сетевую технику, а в часть машины состояний:

created ↓ processing ↓ succeeded или processing ↓ unknown ↓ reconcile ↓ succeeded / failed

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

Поэтому я бы определял идемпотентность так: повторная доставка одного и того же намерения пользователя не должна повторно менять бизнес-состояние.

Запросов может быть много. Намерение пользователя — одно.

И хорошая архитектура должна уметь отличать одно от другого.

Идемпотентность — это не про повтор запроса. Это контроль. | Сетка — социальная сеть от hh.ru