Клиент сделал одну покупку и получил два одинаковых письма с благодарностью. В магазине заказ один, поэтому команда сначала ищет повторный запуск кампании. Но причина может находиться в обмене данными: магазин повторно сообщил об оплате, потому что не получил подтверждение первого запроса.

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

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

Разберём, где нужны такие проверки: при записи события, обновлении заказа, входе в сценарий и отправке. Защита на одном участке не всегда предотвращает повтор на следующем.

Как возникает повтор при исправных системах

Магазин отправил событие оплаты. CDP — платформа объединения клиентских данных — его записала, но соединение оборвалось до подтверждения. Магазин не знает, выполнена ли операция, и отправляет событие снова. Если CDP воспринимает каждое обращение как новую покупку, клиент попадёт в сценарий дважды.

Вебхук — автоматическое уведомление, которое система отправляет другой системе при событии. В документации Stripe для вебхуков описаны возможные повторные доставки и необходимость обработки дубликатов. [1] Конкретные гарантии зависят от источника, поэтому отсутствие повторов в нескольких ручных тестах не является основанием исключать их из архитектуры.

Повтор может появиться и при восстановлении после сбоя, повторном импорте файла или параллельной работе двух интеграций. Защита должна соответствовать смыслу операции, а не только одному способу доставки.

Идентификатор, или ID, — код, по которому различают записи и объекты. В примерах event_id означает код события, order_id — код заказа, customer_id — код клиента. Ключом действия будем называть значение или набор значений, по которым система определяет, выполняла ли она именно эту операцию.

Разделите ID события и ID бизнес действия

ID события позволяет распознать повторную доставку одной записи. ID заказа связывает изменения одной покупки. Для действия CRM может понадобиться третий ключ: конкретный клиент, конкретный заказ и конкретная цель отправки.

Допустим, магазин сформировал два разных события «заказ оплачен» с разными event_id. Дедупликация только по event_id не поможет. Если бизнес допускает одну благодарность за заказ, ключ действия должен учитывать order_id и назначение сообщения.

Что защищаемПример ключаЧто считается новым действием
Доставку событияИсточник + event_idДругое событие источника
Состояние заказаИсточник + order_id + версия измененияСледующая допустимая версия
Благодарность за покупкуКлиент + order_id + тип сообщенияДругая покупка
Выдачу купонаКлиент + ID акции + основаниеНовое основание по правилам акции
Отправку шага цепочкиID участия + ID шагаСледующий шаг или новое допустимое участие

Ключи в таблице — примеры проектирования, а не готовый универсальный стандарт. Для сценария с намеренным повторным входом нужно сохранить такую возможность. Нельзя случайно запретить все будущие покупки человека одним ключом customer_id.

Почему проверка перед записью может не защитить

Два обработчика — программы, которые выполняют полученные операции, — способны одновременно проверить, что ключа ещё нет, и оба выполнить действие. Поэтому операция «занять ключ, если он свободен» должна быть согласованной: например, опираться на уникальность записи и транзакцию в выбранной базе. Транзакция объединяет несколько изменений в одну операцию: они либо подтверждаются вместе, либо отменяются. Но она сама по себе не объединяет изменение базы с отправкой письма внешнему провайдеру.

Храните состояние обработки: принято, выполняется, подтверждено, ошибка или неопределённый результат. Состояние «ключ найден» не всегда означает, что письмо действительно отправлено. Обработчик мог завершиться аварийно после резервирования действия.

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

Граница между базой и провайдером отправки

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

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

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

Как помогает журнал исходящих операций

Изменение бизнес-данных и намерение передать событие можно фиксировать согласованно, а отправку выполнять отдельным процессом. Такой подход известен как transactional outbox, или журнал исходящих событий. Запись об изменении и намерение его передать сохраняются вместе, а отдельный процесс отвечает за отправку. Amazon Web Services (AWS) описывает его для устранения проблемы двух независимых записей и отдельно указывает на необходимость учитывать возможные дубликаты у потребителя. [2]

Для маркетолога практический смысл в том, что оплаченный заказ не должен теряться между сохранением в магазине и передачей в CRM. Но outbox не снимает необходимость защиты последующих действий: повторная доставка остаётся частью возможного поведения.

Также нужно определить срок хранения ключей. Если источник способен повторно прислать историю за полгода, очистка защиты через сутки недостаточна для таких повторов. Срок выбирают по возможному окну повторной доставки, бизнес-смыслу действия и требованиям хранения.

Условный разбор дубля

Клиент получил две благодарности за один заказ. В журнале найдены два разных event_id, один order_id и два запуска сценария. Интеграция продублировала бизнес-факт при повторной выгрузке, поэтому проверка одинакового event_id не сработала.

Исправление: сохранить события для диагностики, обновлять один заказ по его ID и ограничить благодарность одним действием на заказ. Дополнительно проверить версию статуса, чтобы старая запись «создан» не возвращала оплаченный заказ в предыдущую ветку.

Удаление второго письма из отчёта не исправляет проблему. Нужен контроль в момент решения и отправки, а история инцидента должна оставаться доступной для проверки результатов эксперимента и коммуникационной нагрузки.

Минимальный набор испытаний

Повторите одно событие последовательно и одновременно. Отправьте два разных события об одной оплате. Смоделируйте сбой после записи заказа, после резервирования действия и после приёма сообщения провайдером. Затем повторите старую партию после восстановления.

Для каждого случая проверьте число заказов, входов в цепочку, выданных выгод и попыток отправки. Подтверждённые отправки и попытки учитывайте отдельно. Также убедитесь, что следующая настоящая покупка того же клиента проходит как новое действие.

Хороший результат проверки — известное поведение при повторе и при неопределённом ответе. Оно позволяет сокращать дубли, восстанавливать потерянные операции и честно понимать границы гарантий выбранной архитектуры.

Источники

[1] Stripe. Receive Stripe events in your webhook endpoint. Техническая документация.

[2] Amazon Web Services. Transactional outbox pattern. AWS Prescriptive Guidance.