Перейти к содержимому

Event-Driven Architecture: автономия в обмен на консистентность

· 18 мин
Содержание

TL;DR: Event-Driven Architecture даёт сервисам автономию, цена которой – eventual consistency. CQRS разделяет жизненные циклы read и write, и здесь рождается лаг между действием и его отражением в дашборде. Event Sourcing ложится золотом на документы с workflow и плохо на CRUD. Saga решает распределённые транзакции через компенсации, а не через 2PC. Главный контракт – не с архитектурой, а с продактом: какие данные могут отставать, а какие не могут.


Как-то один мой знакомый рассказал историю – работал тогда техлидом в одной b2b-финтех компании, году в 2018-м. Чел может и приукрасил где-то, но суть передам как услышал.

У них в тот день должна была закрыться большая сделка, миллионов на 5, контракт на полгода. Дело важное – CEO лично зашёл посмотреть, как всё пройдет. Стоит у монитора продакта, рядом – тот самый техлид.

Продакт жмёт “Подтвердить”, в интерфейсе появляется зелёная плашка “Подтверждено”. Красота.

CEO переводит взгляд на дашборд рядом – “Воронка за квартал”. А цифра там та же, что была минуту назад. Сделка только что закрылась, а в воронке – ничего.

И CEO продакту:

– Я только что закрыл эту сделку. Почему здесь не видно?

– Появится через минуту, в дашборд попадает немного позже.

– В смысле ПОЯВИТСЯ?!

Лид потом рассказывал – у него в голове в тот момент было три варианта ответа. Технически верный – про event-driven и eventual consistency, на котором CEO на второй фразе уже бы выпал. Политически безопасный – “это баг, разберёмся”. И честный, но именно такой, который CEO слышать не готов: “Этот лаг – цена за то, что у вас система масштабируется. Убрать его можно, но тогда придётся выкинуть половину архитектурных решений за последние 2 года”.

Выбрал второй. Через год об этом пожалел – уже в переговорке с аудитором Big4. Но до этого ещё дойдём.

А статья про то, ЧТО вы покупаете и ЧЕМ платите, переходя от request-driven к async. CQRS, Event Sourcing, Saga, eventual consistency – на живом коде и на двух историях. Одна – того самого лида, пересказываю как слышал. Другая – моя.

Async: инструмент vs мода

Раньше нужно было объяснять зачем вообще async. Сейчас наоборот – команда чувствует себя недостаточно зрелой, если у неё нет event bus.

Знакомо? У вас 12 микросервисов, и чтобы добавить одно поле в карточку клиента, нужно поменять 5 из них, согласовав с 3 командами. Каждый деплой – прогон цепочкой через CI трёх репозиториев, и если один сервис лёг, вся цепочка отваливается. Это не distributed system, это distributed monolith – только PR’ов теперь в 3 раза больше, чем когда всё лежало в одной кодовой базе. Распознать, где проходят настоящие границы между сервисами (а где их просто нарисовали когда-то на доске и не пересматривали) – отдельный разговор; я про него писал в статье про Event Storming.

Async такого не лечит – он просто перераспределяет сложность. И если заранее не подумать, куда её распределять, получится хуже, чем если бы ничего асинхронного вообще не делали.

Udi Dahan в курсе Advanced Distributed System Design даёт рамку, которая переворачивает этот разговор. Вопрос, не в том, как сервисы общаются – через HTTP или через события. Вопрос в том, насколько они связаны: если два модуля обязаны выкатываться вместе, меняться синхронно и ломаться в одном релизе – они один сервис, как бы они ни выглядели в сети. Сетевая граница без разъединения жизненного цикла – это усложнение, а не архитектура. Async – способ расцепить жизненные циклы, а не способ поменять транспорт.

Из этого наглядно видно, когда async реально окупается. Есть 4 узнаваемых сценария, и хотя бы один из них должен срабатывать, чтобы в это ввязываться:

  • Данные с разным темпом изменений. Бронирование меняется в момент самой брони, а аналитика по бронированиям пересчитывается раз в час. Держать их в одной транзакции – конфликт по определению.
  • Модули с разной скоростью эволюции. Уведомления переписывают раз в три месяца, ядро бронирования – раз в год. Если связать их синхронным API, первый начнёт тянуть второй за собой на каждом релизе.
  • Пересечение границ между контекстами. Booking и Payment – разные bounded contexts, разные команды, разные модели. Синхронный вызов между ними со временем превращается в координацию через два issue tracker’а и три чата.
  • Неравномерная нагрузка. Чёрная пятница, понедельничный пик, миграции из легаси. Очередь позволяет обрабатывать заказы в темпе системы, а не клиента.

Если ни одного такого сценария у вас нет – скорее всего, async и не нужен. Грамотный синхронный HTTP с аккуратно размеченными границами модулей решит задачу заметно дешевле. “Пишем event-driven, потому что у нас микросервисы” – это не инженерное решение, а дань моде.

И вот что обычно не проговаривают. Async – не просто отдельный технический выбор; выбрав его, вы одновременно подписались под eventual consistency. Не на случай сбоя, а постоянно, by design. И этот контракт должен обсуждаться с продактом до архитектуры, иначе его обсудит за вас CEO на демо – как в истории выше.

Через год после того демо лид окажется в переговорке с аудитором Big4. Как он туда попал – сейчас расскажу.

CQRS: read и write живут разной жизнью

CQRS – про то, что чтение и запись могут (и часто должны) ходить через разные модели данных. Есть command-сторона (write) и query-сторона (read). Command меняет агрегат, query читает оптимизированную проекцию. Это не два сервиса – это два сценария доступа, которые могут жить в одном процессе и одной БД, или (если нужно) разлететься по разным хранилищам.

Зачем разделять. Бизнес-логика на command-стороне – инварианты, валидация, права. Query-сторона на 90% сводится к “покажи список с фильтром и сортировкой”. Оптимизируются такие сценарии по-разному.

// Command side: меняет агрегат
internal sealed class CancelBookingHandler(
    IBookingRepository bookings,
    ICancellationPolicyProvider policies,
    IClock clock)
{
    public async Task Handle(CancelBooking command)
    {
        var booking = await bookings.GetById(command.BookingId);
        var policy = await policies.GetFor(booking);

        booking.Cancel(clock, policy);

        await bookings.Save(booking);
        // Domain events (BookingCancelled) диспатчатся при Save
    }
}

Command-handler ничего не знает про список бронирований путешественника. Он работает с одним агрегатом и его инвариантами.

// Read side: проекция, обновляющаяся по событиям
internal sealed class ActiveBookingsProjection :
    IEventHandler<BookingConfirmed>,
    IEventHandler<BookingCancelled>
{
    public async Task Handle(BookingConfirmed @event)
    {
        await _db.ActiveBookings.Add(new ActiveBookingView
        {
            BookingId = @event.BookingId,
            TravelerName = @event.TravelerName,
            Destination = @event.Destination,
            DepartureDate = @event.Departure
        });
    }

    public async Task Handle(BookingCancelled @event)
    {
        await _db.ActiveBookings.Remove(@event.BookingId);
    }
}

ActiveBookingView – не агрегат. Это денормализованная читалка под конкретный экран. Хочется на дашборде показать “10 ближайших отлётов по департаменту” – заводим вторую проекцию: UpcomingDeparturesProjection. Третий экран – третья проекция. Каждая обновляется по тем же событиям, но хранит свою форму данных.

И вот здесь проявляется eventual consistency. Между bookings.Save(...) и моментом, когда ActiveBookingsProjection.Handle(BookingConfirmed) дописал строку в read-store – проходит время. Сколько – зависит от инфраструктуры: миллисекунды на in-process bus, секунды на Kafka, минуты на batch-процессор. Но не ноль. Никогда не ноль.

Помните ответ лида “появится через минуту”? Это была проекция дашборда, агрегирующая события из четырёх контекстов раз в 30 секунд. Технически всё работало правильно. CEO ждал другого ответа.

Год спустя – аудитор Big4 на проверке в конце года. Ему нужны показатели выручки за прошедший год. Финансы выгрузили отчёт из DWH. Аудитор сравнил его с цифрами из операционной системы. Цифры разные. Оба отчёта корректны.

Объяснение оказалось простое: 31 декабря в 23:13 платёжная интеграция получила batch из 47 транзакций от провайдера. Транзакции были обработаны операционной системой, событие PaymentSettled ушло в корпоративную шину. Проекция в DWH забирала события раз в 30 минут. Следующий запуск – 1 января в 00:00. К этому моменту финансы уже зафиксировали закрытие года.

47 минут необработанных событий пересекли границу финансового периода.

Урок, который я из этой истории вынес: проекция без владельца – бомба замедленного действия. Каждая read-модель – это отдельный “сервис” со своими SLA, своими алертами и человеком, который отвечает за то, что она не отстала больше, чем разрешено бизнесом. Проекция без владельца живёт до первого квартального отчёта. Если у вас CQRS с двумя проекциями – у вас должно быть два владельца. Не один на всё.

Event Sourcing: когда золото, когда overkill

В Event Sourcing состояние системы хранится не “текущей строкой в таблице”, а последовательностью событий. Чтобы получить текущее состояние – проигрываем события с начала; состояние становится проекцией истории.

Звучит академично. На практике – окупается в нескольких узких классах задач. Первый – документы с жизненным циклом и аудитом по требованию на любую дату. Второй – банковские счета и журнал транзакций, где каждое изменение имеет бизнес-последствие. Во всех остальных случаях – overkill.

Когда ES – overkill:

  • CRUD над товарами в корзине. Поменял количество, поменял адрес. Никому не важно, что вчера в 14:30 вы поменяли количество с 2 на 3, а потом обратно. Хранить всю историю – нагрузка на инфраструктуру без выгоды.
  • Простые домены, где изменения состояния редко имеют значение для бизнеса. Профиль пользователя. Настройки. Каталог продуктов.
  • Маленькие команды без опыта работы с проекциями. ES требует дисциплины: версионирование схемы событий, периодические снэпшоты для производительности, механика повторных проигрываний истории. Если для команды это впервые – цена входа крайне высокая.

Когда ES – золото: документы с workflow + audit-on-demand.

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

public record PurchaseRequestCreated(
    PurchaseRequestId Id, BuyerId Buyer, Money Amount, DateTimeOffset At
    ) : IEvent;

public record ApprovedByWarehouseManager(
    PurchaseRequestId Id, ManagerId Manager, DateTimeOffset At
    ) : IEvent;

public record EscalatedToFinance(
    PurchaseRequestId Id, string Reason, DateTimeOffset At
    ) : IEvent;

public record ApprovedByCFO(
    PurchaseRequestId Id, CFOId CFO, string Comment, DateTimeOffset At
    ) : IEvent;

public record ApprovalRevoked(
    PurchaseRequestId Id, ActorId By, string Reason, DateTimeOffset At
    ) : IEvent;

public record SentToSupplier(
    PurchaseRequestId Id, SupplierId Supplier, DateTimeOffset At
    ) : IEvent;

public record PartiallyFulfilled(
    PurchaseRequestId Id, Money DeliveredAmount, DateTimeOffset At
    ) : IEvent;

Что мы получили на этом проекте, чего не было бы на классическом CRUD:

Audit-on-demand. Регулятор приходит: “Покажите состояние заявки X на 15 марта в 18:00”. Проигрываем события до этой временной отметки – получаем точное состояние документа на тот момент. Без журнала аудита, без отдельной таблицы изменений, без “ой, мы не логировали эту колонку”.

Compensating events. CFO одобрил, потом передумал. На CRUD мы бы делали UPDATE status='draft', теряя факт одобрения. На ES добавляем событие ApprovalRevoked – одобрение по-прежнему в истории, просто отменено новым фактом. Аудит видит всё.

Новые проекции бесплатно. Бизнес попросил отчёт “сколько заявок проходило через эскалацию к CFO в каждом квартале”. На CRUD – пишем новый ETL-скрипт и боремся с тем, что часть данных в логах, часть в основной БД. На ES – пишем новую проекцию по существующим событиям. Вот и всё.

Звучит абстрактно, пока не попадётся конкретный кейс. Приходит контрагент-поставщик: “мы отгрузили по заявке PR-12847 на 4.2 млн, акт не подписываете уже две недели”. Открываем текущее состояние заявки – 3.6 млн, пометка “пересмотрено закупками”. Поставщик настаивает: “у нас вся переписка есть, вы одобрили наши условия 15 марта в 18:43”. На классическом CRUD дальше был бы долгий разбор – поднимать бэкапы, дёргать DBA, пытаться восстановить таблицу на ту дату. У нас – replay событий до 18:43 15 марта – 3 минуты и перед глазами снимок заявки ровно в тот момент. Поставщик оказался прав: одобрение было на 4.2 млн, закупки потом зарезали сумму без повторного согласования. Вопрос закрыли в тот же день.

С чем нужно разобраться заранее:

  • Snapshots. Заявка с пятилетней историей не должна реплеиться с нуля. Раз в N событий сохраняем snapshot, реплеим только хвост.
  • Schema versioning. Через два года вы добавите поле в ApprovedByCFO. Старые события без этого поля никуда не денутся. Версионирование схемы событий – обязательная часть процесса, не опциональная.
  • Projections rebuild. Поменяли логику проекции – нужно пересобрать с нуля. На больших объёмах занимает часы, поэтому планируйте окна.

Правило, которое я применяю с тех пор: ES – не для “сохранить всё на всякий случай”. ES – для документов, чья история обязана сохраняться по бизнес-причинам или по требованию регулятора. Для всего остального – overkill, и расплата за него приходит на третий год эксплуатации, когда схема событий разъезжается, снепшоты не успевают, а команда уже не помнит почему это было хорошей идеей.

Saga: multi-leg trip и кто кого координирует

Тикет упал в 14:47. Финансы пишут в Slack: “у клиента 3 списания за один и тот же перелёт, а бронь одна. Что происходит?”.

Открываю логи Booking-сервиса. Кладу рядом логи Payment. Сравниваю timestamp’ы. Картина начинает складываться.

Бронь была multi-leg: рейс Москва → Франкфурт, ночь в отеле, рейс Франкфурт → Сан-Франциско. Корпоративная поездка топ-менеджера, бюджет согласован, пятничный рейс перед понедельничной встречей.

Сценарий, который пошёл не так:

  1. 14:32:08 – клиент нажимает “Подтвердить бронирование”. Создаётся BookingDraft со статусом Pending.
  2. 14:32:09 – команда BookFlight(MOW→FRA) уходит в Booking-сервис. Сегмент A создан. Событие SegmentBooked(A).
  3. 14:32:11 – команда BookHotel(FRA, 2 nights). Отель забронирован. Событие SegmentBooked(B).
  4. 14:32:14 – команда BookFlight(FRA→SFO). Авиакомпания возвращает 409: “no seats in business class on this date”.
  5. 14:32:14BookingDraft.Status = Failed.
  6. 14:32:14 – клиент видит “ошибка, попробуйте снова”. Никаких упоминаний, что первые два сегмента уже забронированы и оплата уже инициирована.

Через 45 секунд клиент нажимает “Подтвердить” снова. Система не помнит про предыдущую попытку – у неё новый http-запрос. Новая попытка стартует. Снова падает на третьем сегменте. Снова pre-auth списание. И так три раза, пока клиент не написал в саппорт.

Это классическая задача распределённой транзакции. Booking-сервис, Hotel-провайдер, Flight-провайдер – три разных системы, каждая со своим API, своими таймаутами, своими статусами. Атомарность через 2PC – невозможна (внешние системы не предоставляют 2PC), нежелательна (блокирующие транзакции через сеть с непредсказуемыми задержками – путь в ад) и неправильна по форме задачи (это не консистентность БД, это бизнес-процесс).

Pat Helland в эссе “Life beyond Distributed Transactions” формулирует ровно это: распределённые транзакции – попытка натянуть локальную ACID-семантику на систему, у которой её нет и не будет. Альтернатива – принять, что бизнес-операции состоят из независимых атомарных шагов, каждый из которых может быть скомпенсирован отдельным шагом. И потратить силы на идемпотентность каждого шага, а не на двухфазный коммит.

Это saga – бизнес-транзакция, идущая через несколько шагов, при ошибке любого шага запускающая компенсацию для уже выполненных. Саги бывают двух видов: choreography и orchestration. Покажу обе на нашем сценарии.

Choreography. Каждый сервис подписывается на события и реагирует. Никакого центрального координатора.

internal sealed class FlightFailureChoreographer
    : IEventHandler<FlightBookingFailed>
{
    public async Task Handle(FlightBookingFailed @event)
    {
        // Откатываем уже забронированные сегменты этой брони
        await _events.Publish(new ReleaseHotelReservation(@event.BookingId));
        await _events.Publish(new ReleaseFlightSegments(@event.BookingId));
        await _events.Publish(new RefundPreAuth(@event.BookingId));
    }
}

Hotel-сервис подписан на ReleaseHotelReservation, Booking – на ReleaseFlightSegments, Payment – на RefundPreAuth. Каждый отрабатывает свой шаг компенсации независимо.

Плюсы choreography: никакой single-point-of-coordination, сервисы автономны, можно деплоить независимо. Минусы: flow невидим. Чтобы понять “что происходит при отмене брони” – нужно прочитать обработчики в трёх разных сервисах. Plus correlation ID и distributed tracing, иначе через полгода никто не вспомнит, кто на что подписан.

Orchestration. Явный координатор – оркестратор – держит state machine процесса.

public class BookTripSaga
{
    public async Task<TripBookingResult> Run(BookTripRequest request)
    {
        var booking = await _bookings.CreateDraft(request);

        var flightA = await _flights.Book(request.OutboundLeg, booking.Id);
        if (!flightA.IsSuccess)
            return TripBookingResult.Failed(flightA.Error);

        var hotel = await _hotels.Book(request.HotelStay, booking.Id);
        if (!hotel.IsSuccess)
        {
            await _flights.Cancel(flightA.SegmentId);
            return TripBookingResult.Failed(hotel.Error);
        }

        var flightB = await _flights.Book(request.ReturnLeg, booking.Id);
        if (!flightB.IsSuccess)
        {
            await _hotels.Cancel(hotel.ReservationId);
            await _flights.Cancel(flightA.SegmentId);
            await _payments.RefundPreAuth(booking.Id);
            return TripBookingResult.Failed(flightB.Error);
        }

        await _bookings.Confirm(booking.Id);
        return TripBookingResult.Success(booking.Id);
    }
}

Flow виден целиком. Какой шаг что делает, в каком порядке откатывается – всё на одной странице. Цена – есть центральный компонент, который должен жить (high availability), и у которого свой жизненный цикл деплоя. Если оркестратор упал в середине саги – нужны механика возобновления (event-sourced state, retry, dead letter).

Обе формы валидны. Выбор зависит от ответа на один вопрос: важнее прозрачный процесс или автономия?

Если процесс редко меняется, у него стабильная форма, его нужно отлаживать и аудитить – orchestration. Прозрачный флоу дороже, чем автономия команд. Если процесс меняется часто, разные команды могут по-разному реагировать на одно событие, нужна автономия деплоев – choreography. Цена в потере видимости приемлема.

На том multi-leg booking мы перешли с choreography на orchestration. Не потому что одно объективно лучше другого, а потому что когда саппорт-инженер третий раз за неделю не мог объяснить клиенту, на каком этапе застряла отмена бронирования – мы поняли, что инвестиция в видимый процесс окупится. Choreography хорошо выглядит на whiteboard’е архитектора и плохо выглядит на экране саппорта в 23:47 в субботу.

И главный урок, который многие пропускают: компенсация ≠ откат. Иногда компенсирующее действие – не “отменить и забыть”, а “отметить как требующее ручного вмешательства и эскалировать”. Если возврат от провайдера платежей (refund) не прошёл три раза подряд – не делайте четвёртую попытку в цикле, ставьте задачу человеку. Финансы предпочтут увидеть тикет, чем обнаружить через месяц $5000 в подвешенном состоянии.

Eventual consistency: жить с ней

Вернёмся в ту переговорку с аудитором, которую я анонсировал в начале.

Важный для компании аудит, январь, годовой финансовый отчёт. Аудитор – назовём его Артём. На столе два листа: revenue report из операционной системы, revenue report из DWH. Цифры различаются на 5 миллионов рублей.

– Объясните разницу.

Финансы паникуют. Технари в защите. Лид объясняет про event-driven, batch projections, 47 минут необработанных событий, пересёкших границу финансового периода. Артём кивает. Записывает. Спрашивает:

– Какой из двух отчётов верный?

– Оба корректны. Они срезают данные в разные моменты времени.

– Мне нужен один. Тот, который вы официально подаёте.

Потом лид мне рассказывал: “Я понял, что объяснил eventual consistency правильно технически – и абсолютно бесполезно для аудита. Артёму не нужно понимание архитектуры. Ему нужно одно число, под которым стоит подпись”.

Pat Helland в эссе “Memories, Guesses, and Apologies” предлагает систему, которая сильно меняет как со всем этим жить. Разделить три вещи в распределённой системе:

  • Memories – факты о произошедшем – события. Они immutable.
  • Guesses – проекции, дашборды, агрегаты. Догадки на основе фактов. Они могут отставать, они могут оказаться неверными, если факт пришёл позже, чем догадка была сформирована.
  • Apologies – компенсирующие действия для случаев, когда догадка оказалась неверной. Корректирующие транзакции, объяснения клиенту, перерасчёт.

Хорошо спроектированная распределённая система знает про все три. Она не делает вид, что guesses – это источник истины. И она проектирует apologies заранее, а не изобретает на ходу, когда аудитор приходит с вопросами.

После того аудита они изменили дашборд. Каждая агрегированная цифра теперь идёт с временной отметкой: “Воронка за квартал: 275 млн ₽ на 14 янв, 14:32:09”. Если последнее обновление было 30 секунд назад – это 30 секунд назад, и пользователь это видит. Маленькое изменение в UI. Большое – в том, как пользователь интерпретирует число.

Несколько UI-паттернов, которые работают с EC, а не против неё:

  • Временная отметка “на момент X” у каждой агрегированной цифры. Цифра без отметки – это претензия на источник истины, которой у вас нет.
  • Оптимистичные обновления с откатом. Пользователь нажал “отменить” – интерфейс сразу показывает “отменено”, в фоне идёт saga. Если saga не прошла – откатываем интерфейс, показываем причину. Отзывчивость UI отвязывается от задержек бэкенда.
  • “Обновляется…” вместо “обновлено”. Прогресс асинхронной операции – полноценный элемент UI, а не внутренняя деталь сервера. Пользователь видит состояния процесса, а не просто “успех/ошибка”.
  • Идемпотентность по намерению, а не по запросу. Кнопка “Подтвердить”, нажатая трижды за пять секунд, – это одно намерение. Сервер должен это понимать, иначе получите три брони и заблокированную карту в субботу днём.

Где EC недопустима – узкий, но строгий список:

  • Резерв последних мест. Последнее место в бизнес-классе. Последний номер в отеле. Здесь нужна strong consistency – иначе двое пассажиров улетят с подтверждениями на одно место.
  • Отчётные границы. Закрытие месяца, квартала, года. Финансовая отчётность регулируется законом. Нельзя “догнать” 47 минут после полуночи 1 января.
  • Требования регулятора. GDPR-удаление данных, KYC-проверки, AML-блоки. Когда регулятор спрашивает “стёрто ли это?” – ответ “да, в течение 30 секунд оно стерётся” не годится.

В остальных случаях EC приемлемо – если вы об этом договорились с продактом. И это – самая важная часть. Udi Dahan в ADSD говорит, что eventual consistency – не workaround, это природа распределённых систем. С ней придётся считаться. Но “считаться” означает проектировать осознанно: где мы согласны на лаг, какой максимум лага, как пользователь это видит, какие apologies мы готовим заранее.

Это контракт. Подписывать его нужно с продактом до архитектуры, а не после жалоб от CEO.

В итоге

Возвращаюсь к той истории. После демо архитектуру они не переписывали. Изменили две вещи в интерфейсе. Первое – добавили временную отметку “на момент X” к каждой агрегированной цифре. Второе – на странице сделки, после нажатия “Подтвердить”, показали локальный счётчик: “Сделка 5 млн ₽ подтверждена. Появится в воронке в течение 60 секунд”. Цена изменения – два дня работы фронта. Цена ошибки до этого – один разговор с CEO, на который лид не знал, что ответить.

Три вещи, которые стоит унести.

EDA даёт автономию сервисов в обмен на eventual consistency. Не за просто так. Это конкретный контракт: вы соглашаетесь, что часть данных в системе будет отставать. Не “может отставать”, а будет – всегда, by design.

CQRS, Event Sourcing, Saga – это инструменты под конкретные боли, не паттерны под чеклист. CQRS оправдан, когда read и write имеют разные жизненные циклы. ES – для документов с workflow и audit-on-demand. Saga – для бизнес-процессов через несколько контекстов с явными компенсациями. Применять их потому что “у всех микросервисов так” – дороже, чем не применять.

Eventual consistency – это контракт, который подписывается с продактом, а не с архитектором. Где мы согласны на лаг, какой максимум, как пользователь это видит. Если продакт не подписался – не стройте event-driven систему. Стройте distributed monolith. Он хотя бы предсказуем.

В следующей статье – про самое злоупотребляемое слово в IT после Agile: техдолг. Спросите 5 разработчиков, что это – получите 7 определений. Разберём, что на самом деле имел в виду Каннингем, как измерить в деньгах и как перестать просить “спринт на рефакторинг”.

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


Что почитать

  1. Pat Helland – “Life beyond Distributed Transactions: An Apostate’s Opinion” и “Memories, Guesses, and Apologies”. Две короткие статьи практика с 30-летним опытом распределённых систем (Tandem, Microsoft, Amazon, Salesforce). Первая – манифест против 2PC. Вторая – рамка memories/guesses/apologies, которая меняет, как вы думаете про EC. Без них любое чтение про распределённые транзакции – неполное.
  2. Udi Dahan – курс Advanced Distributed System Design. Платный, но один из лучших материалов по EDA, CQRS, сагам и проектированию сервисных границ. Особенно сильна его рамка про связанность жизненных циклов как первичный вопрос архитектуры, до того как обсуждать “сообщения vs HTTP”.
  3. Martin Kleppmann“Designing Data-Intensive Applications”. Главы про replication, partitioning, transactions, consistency models – обязательные. Talk “Turning the Database Inside Out” – переворачивает perspective: лог как первичная абстракция данных, а БД – как материализованное view над логом.
  4. Greg Young – talks про CQRS и Event Sourcing на YouTube. Создатель термина CQRS, евангелист ES. Рассказчик уровня stand-up’а: часовой talk идёт как фильм, при этом плотность практической информации – максимальная.
  5. Chris Richardson“Microservices Patterns”. Каноничный голос за orchestration-сагу. Если хотите глубже – это книга. Особенно полезны главы про Saga и Transactional Outbox.
Поделиться: Telegram X