TGViewer
.NET Разработчик .NET Разработчик @netdeveloperdiary · 6.74K subscribers
Post #3208 1.57K
День 2680. #ЗаметкиНаПолях
Когда Сценарий Выполняется Наполовину: Проектирование с Учётом Частичного Сбоя в .NET. Окончание

Начало

Стратегии обработки побочных эффектов
1. Выносим транзакционные задачи на конец процесса
Первый шаг — механический. Все транзакционные операции должны быть зафиксированы в последнюю очередь, после того как все внешние вызовы либо успешно завершены, либо их сбои явно обработаны.
var order = Order.Create(…);

var charge = await
payments.ChargeAsync(…);

if (charge.IsFailure) return charge;

order.MarkPaid(charge.Value.TransactionId);

// …

orders.Insert(order);

await unitOfWork.SaveChangesAsync(ct);
return Result.Success();

Не всегда это возможно, и это нормально. Суть в том, чтобы убедиться, что если вы выполняете операцию подтверждения, то уже сделали всё, что обещает эта операция.

2. Выносим необратимые побочных эффекты за пределы
Здесь применяется паттерн Outbox. Вместо прямой отправки email, сгенерируйте событие домена OrderPlaced и позвольте диспетчеру исходящих сообщений обработать его после подтверждения транзакции:
// …
orders.Insert(order);
order.Raise(new OrderPlacedEvent(order.Id));
await unitOfWork.SaveChangesAsync(ct);
// …

Отправка email больше не забота этого метода. Если транзакция подтверждена, событие подтверждается вместе с ней в рамках той же операции записи. Если нет, событие никогда не покидает БД, и email никогда не отправится. Отдельный обработчик преобразует события в emailы, используя собственные повторные попытки и собственные гарантии идемпотентности.

3. Делаем внешние вызовы идемпотентными или компенсируемыми
Если вызов платежа успешен, а транзакция отменена, мы взяли лишние деньги. Точно нельзя молчаливо мириться с неудачей.

Подход А: Ключи идемпотентности
Большинство платежных систем позволяют прикрепить ключ идемпотентности к платежу. Повторная попытка с тем же ключом возвращает исходный результат. Естественный ключ - ID заказа:
var charge = await payments.ChargeAsync(
new ChargeRequest
{
OrderId = order.Id,
Amount = order.Total,
IdempotencyKey = order.Id.ToString()
},
ct);

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

Подход B: Возврат через событие домена
Если повторить операцию нельзя — пользователь отказался от попытки, запрос отменён или ошибка является необратимой, деньги реальны и должны быть возвращены. Сделаем ошибку событием и вернём средства:
// …
try
{
// …
await unitOfWork.SaveChangesAsync(ct);
}
catch (Exception ex)
{
await outbox.PublishAsync(
new PaymentFailedEvent(
order.Id,
charge.Value.TransactionId,
order.Total,
Reason: ex.Message),
ct);
throw;
}
// …

Фоновый потребитель подписывается на событие PaymentFailedEvent и инициирует возврат средств, используя идентификатор транзакции в качестве ключа идемпотентности. Это превращает сложную межпроцессную компенсацию в обычный, наблюдаемый, повторяемый обработчик сообщений.

Паттерн Сага
Описанные выше стратегии работают, когда один вариант использования координирует небольшое количество побочных эффектов в одном сервисе. Как только работа охватывает несколько сервисов и должна сохраняться после перезапуска процесса, лучше использовать паттерн Сага.
Простое правило: если вы можете уместить логику восстановления в своей голове, достаточно хорошо разработанного варианта использования. Если нет, используйте паттерн Сага.

Источник:
https://www.milanjovanovic.tech/blog/when-your-use-case-half-succeeds-designing-for-partial-failure-in-dotnet
  • 👍 10
  • 👎 1
More from @netdeveloperdiary
  1. Sep 26, 2026День 2796. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Продолжение Начало Три…
  2. Sep 25, 2026День 2795. #ЗаметкиНаПолях #AI Рабочий процесс с Copilot для .NET. Начало Проблема с позиц…
  3. Sep 24, 2026День 2794. #Оффтоп #Здоровье Сегодня будет необычный пост. Завтра в Москве стартует конфер…
  4. Sep 23, 2026День 2793. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  5. Sep 22, 2026День 2792. #ЗаметкиНаПолях #SQL 10 Редких Возможностей SQL, Которые Стоит Знать Каждому. Ч…
  6. Sep 21, 2026🔍Тестовое собеседование с Senior C# разработчиком уже завтра 22 сентября(уже завтра!) в 1…
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →