Опирается на правила:
AUTH-19из Auth Patterns Style Guide → раздел 8. Идемпотентность как часть auth-контракта.
Важно знать
- Money-команды требуют
Idempotency-Keyheader — обязательный.- Повторный вызов с тем же ключом возвращает прежний результат, не создаёт дубль.
- Защита от retry клиента, retry мобильного приложения, retry в S2S, network timeout.
- Без Idempotency-Key два списания одного клиента — money-инцидент.
- Идемпотентность — это auth-контракт: «правильно подписанный запрос обрабатывается ровно один раз».
- Тот же ключ + другая команда →
409 Conflict.
Money-команды не должны выполняться дважды от одного клиента. Кажется очевидно, но сетевой timeout не означает, что write не произошёл — он означает, что мы не знаем, произошёл или нет. Retry без идемпотентности → двойное списание. UCP формулирует требование как часть auth-контракта, не optional optimization.
Какие команды требуют Idempotency-Key
AUTH-19: критерий — money или резерв.
| Команда | Нужен Idempotency-Key? |
|---|---|
CreateOrder | Да — резервирование/payment |
ConfirmPayment | Да — money |
RefundPayment | Да — money |
ChargeBalance | Да — money |
CreditBalance | Да — money |
ReserveInventory | Да — резерв |
CancelOrder | Да — может откатить payment |
UpdateOrderStatus | Нет (если не triggers payment) |
GetOrderById | Нет (read-only) |
UpdateUserProfile | Нет (не money) |
Общее правило: «если retry с тем же payload может потребовать compensation на стороне backend — нужен Idempotency-Key».
Контракт
Клиент:
POST /orders
Authorization: Bearer ...
Idempotency-Key: 0193a8f3-7c21-7e3f-9b4a-...
Content-Type: application/json
{ "customerId": 42, "items": [...] }
Backend:
- Первый вызов: обрабатывает, сохраняет
(idempotency_key, command_hash, response)вidempotency_record, возвращает201 Created+ body. - Повтор с тем же ключом + тем же payload: возвращает сохранённый response (тот же status, тот же body). Не создаёт дубль.
- Тот же ключ + другой payload:
409 Conflict— клиент пытается переиспользовать ключ. - Без header:
400 Bad Request—Idempotency-Keyrequired.
Реализация — см. Distributed → idempotency.
@RestController
@RequestMapping("/orders")
@RequiredArgsConstructor
public class OrderController {
private final UseCaseDispatcher dispatcher;
private final IdempotencyRecordRepository idempotencyRepository;
@PostMapping
@PreAuthorize("hasRole('customer')")
public ResponseEntity<OrderResponse> create(
@RequestHeader("Idempotency-Key") String key,
@RequestBody @Valid CreateOrderRequest request
) {
var commandHash = HashUtil.sha256(request);
var existing = idempotencyRepository.findByKey(key);
if (existing.isPresent()) {
var record = existing.get();
if (!record.commandHash().equals(commandHash)) {
throw new IdempotencyConflictException(key);
}
return ResponseEntity.status(record.httpStatus())
.body(record.response(OrderResponse.class));
}
var order = dispatcher.dispatch(new CreateOrderCommand(key, request));
var response = OrderResponse.from(order);
idempotencyRepository.save(key, commandHash, response, 201);
return ResponseEntity.status(201).body(response);
}
}
Почему «часть auth-контракта»
Idempotency-Key не optional, он не feature. Он часть того, что значит «правильно подписанный запрос обрабатывается». UCP формулирует это в auth-контексте, потому что:
- Без него auth бесполезен. Атакёр с украденным token может явно retry-нуть money-команду — без idempotency списание дублируется.
- Без него legitimate retry становится атакой на пользователя — клиент думает «не отправилось», retry, два списания.
- Это контракт между client и server, как
Authorizationheader — без него запрос отклоняется.
Поэтому endpoint money без Idempotency-Key header — 400 Bad Request, не оптимизация.
Money — двойная защита
Дополнительно к application-уровню idempotency_record, money-операции защищаются БД unique-constraint:
CREATE TABLE payment (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
order_id bigint NOT NULL,
idempotency_key text NOT NULL,
amount numeric(19,4) NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (order_id, idempotency_key)
);
Если idempotency_record пропустил retry (например, разные clients использовали разные ключи), UNIQUE constraint на (order_id, idempotency_key) ловит дубль на уровне БД.
Подробнее — Distributed → idempotency R-DIST-IDEM-4.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Money-endpoint без Idempotency-Key обязательного | AUTH-19 | header required, иначе 400 |
Idempotency-Key optional для money | AUTH-19 | обязателен |
| Retry с новым Idempotency-Key | AUTH-19 | один ключ на бизнес-операцию |
idempotency_record без TTL | AUTH-19 | 24-72 часа |
| Idempotency только на application, без БД UNIQUE | AUTH-19 | двойная защита для money |
| Одинаковый ключ для разных команд | AUTH-19 | проверка command_hash → 409 |
| Idempotency-Key только в URL, не в header | AUTH-19 | header (URL meant для resource id) |
Куда дальше
- Auth → раздел 8. Идемпотентность — нормативные формулировки.
- Distributed → idempotency — детали
idempotency_record, двойная защита. - Kafka → idempotent consumer —
eventIdкак Idempotency-Key для downstream HTTP. - Distributed → eventual consistency — read-your-writes без double-charge.
- REST API → headers —
Idempotency-Keyheader convention. - Resilience → retry — retry только при идемпотентности.