Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Architecture Docs
- SECURITY.md — безопасность.
- DATABASE_SCHEMA.md — база данных.
- QUEUES_AND_EVENTS.md — события и очереди.
- TRANSACTIONAL_OUTBOX.md — надёжная публикация доменных событий после commit.
- PAYMENTS_AND_HOLDS.md — оплата и временное удержание слота.
- OBSERVABILITY.md — health, ready, metrics и logs.
- LOGGING.md — structured JSON logs, Loki/Vector и trace correlation.
Expand Down
65 changes: 65 additions & 0 deletions docs/architecture/TRANSACTIONAL_OUTBOX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Transactional Outbox

Этот документ описывает целевой outbox-паттерн для событий бронирования.

## Проблема

`booking-service` сейчас меняет PostgreSQL и затем публикует событие в Redis
Streams. Между этими действиями возможен сбой:

1. транзакция PostgreSQL успешно создала или отменила бронь;
2. процесс упал или Redis временно недоступен;
3. событие `booking.created` или `booking.cancelled` не попало в stream.

В этом случае база уже содержит новый факт, но downstream-сервисы его не увидят:
notification-service не отправит сообщение, analytics-service не обновит метрики,
calendar sync может пропустить изменение.

## Целевое решение

Событие нужно писать в таблицу `outbox.OutboxEvent` в той же PostgreSQL
транзакции, где меняется `booking.Booking`.

Минимальная форма записи:

| Поле | Назначение |
| --- | --- |
| `id` | уникальный event id |
| `aggregateType` | например `booking` |
| `aggregateId` | id брони |
| `eventName` | `booking.created`, `booking.cancelled`, `booking.completed` |
| `payload` | JSON payload для Redis Streams |
| `status` | `pending`, `published`, `failed` |
| `attempts` | сколько раз worker пытался опубликовать событие |
| `nextAttemptAt` | время следующей попытки |
| `publishedAt` | когда событие ушло в Redis |
| `createdAt` | когда событие записано |

Отдельный outbox worker читает `pending` записи, публикует payload в Redis
Streams и помечает запись как `published`. Доставка становится at-least-once:
consumer обязан быть идемпотентным по `eventId`.

## Первые события

Начать стоит с событий, которые уже публикует `booking-service`:

- `stream:booking.created`;
- `stream:booking.cancelled`.

После этого тем же механизмом можно покрыть `stream:booking.completed`, который
сейчас публикуется worker-service после автоматического завершения брони.

## Правила реализации

- Нельзя публиковать Redis событие внутри бизнес-транзакции напрямую.
- `OutboxEvent` пишется до commit вместе с изменением брони.
- Worker делает publish после commit и ретраит временные ошибки Redis.
- Payload должен содержать стабильный `eventId`.
- Consumers должны дедуплицировать `eventId`, потому что доставка at-least-once.
- После превышения лимита попыток запись остаётся в `failed` для ручного replay.

## Проверка готовности

Фича считается готовой, когда тест покрывает сценарий: booking transaction
создала бронь и outbox event, Redis publish временно упал, worker позже
повторил publish без потери события.
Loading