Самый распространённый подход — Idempotency Key. Клиент генерирует уникальный ключ (UUID) и отправляет его в заголовке. Сервер хранит маппинг ключ → результат.
func CreatePayment(ctx context.Context, key string, req PaymentReq) (*Payment, error) {
// проверяем, был ли уже такой запрос
if cached, err := redis.Get(ctx, "idempotency:"+key); err == nil {
return cached, nil
}
payment, err := processPayment(ctx, req)
if err != nil {
return nil, err
}
// сохраняем результат на 24 часа
redis.Set(ctx, "idempotency:"+key, payment, 24*time.Hour)
return payment, nil
}
Альтернативы: UPSERT в БД (ON CONFLICT DO NOTHING), уникальный constraint на бизнес-ключ, версионирование ресурсов (optimistic locking). Для платежей idempotency key обязателен — Stripe, T-Bank, все платёжные системы его поддерживают. Храни результаты минимум 24 часа, потому что клиент может повторить запрос не сразу.