DDD в iGaming-ядре
iGaming-платформа — это сложная доменная система на стыке финансов, развлечений и комплаенса. DDD помогает удерживать сложность: выделяет bounded contexts, фиксирует ubiquitous language, защищает инварианты агрегатами, упрощает интеграции через антикоррупционные слои и делает поведение системы прозрачным благодаря доменным событиям.
1) Карта доменов и bounded contexts (стратегический дизайн)
Рекомендуемая декомпозиция:- Player/KYC Context — регистрация, верификация, лимиты ответственной игры, статусы KYC/AML.
- Wallet/Ledger Context — балансы, резервирования, проводки, мультивалютность, курсы.
- Betting Context — ставки/тикеты, пара/исходы, котировки, расчет (settlement), отмена.
- Casino/Game Round Context — сессии, раунды, спины, RTP-контроль, лимиты ставок.
- Bonus/Promo Context — правила бонусов, вагер, эквайринги бонусных средств, анти-абьюз.
- Risk/Fraud Context — скоринг, поведенческие сигналы, триггеры блокировок/тайм-аутов.
- Payments Context — депозиты/выводы, статусы платежных шлюзов, chargeback-события.
- Compliance/Reporting Context — отчеты регуляторам, санкционные списки, аудит.
- Content/Provider Integration Context — интеграция с провайдерами игр, каталоги, тех. статусы.
- Analytics/Read Models — проекции и витрины под продуктовые чтения.
2) Ubiquitous language: ядро терминов
Player (Игрок), Session (Сессия), GameRound (Раунд), Bet/Ticket (Ставка),
Ledger Entry (Проводка), Hold/Reserve (Резерв), Settlement (Расчет),
Bonus Credit / Bonus Balance, Wagering Requirement (Вейджер),
KYC Tier, Limit (депозит/сессия/потери), Self-Exclusion,
Provider Game, RTP Window, Risk Flag, Compliance Case.
Эти названия одинаково употребляются в коде, БД, документации, тестах и интерфейсах.
3) Агрегаты и инварианты (тактический дизайн)
3.1 Wallet (Aggregate: `Wallet`)
Инварианты:- Баланс не уходит в минус.
- Резерв + доступный ≤ общий баланс.
- Проводка атомарна и идемпотентна (по `operation_id`).
- `Wallet.Reserve(amount, reason, op_id)` → `WalletReserved`
- `Wallet.Commit(op_id)` → `WalletCommitted`
- `Wallet.Rollback(op_id)` → `WalletRolledBack`
Граница: Wallet не знает про Bet/Bonus; он обслуживает операции проводок и резервов.
3.2 Bet/Ticket (Aggregate: `Bet`)
Инварианты:- Ставка может быть принята только в активном окне котировок; сумма ≤ лимита игрока/сессии.
- После `Settled` статус «финализирован»; повторный расчет допускается только через компенсирующие операции (void/recalc) с четким аудитом.
- `Bet.Place(player_id, amount, price, op_id)` → `BetPlaced` (требует Wallet.Reserve)
- `Bet.Settle(outcome, payout)` → `BetSettled` (требует Wallet.Commit/Release)
- `Bet.Void(reason)` → `BetVoided`
Граница: Bet не «лезет» в Wallet — обращается через доменные команды/оркестрацию.
3.3 GameRound (Aggregate: `Round`)
Инварианты:- Каждый спин/раунд имеет уникальный `round_id` и связанную сумму ставки/выигрыша.
- RTP-окно не превышает заданные пороги (на уровне провайдера + локальные правила).
- `Round.Started`, `Round.Staked`, `Round.Resulted`, `Round.Closed`.
3.4 Bonus (Aggregate: `BonusGrant`)
Инварианты:- Вейджер уменьшается только от валидного оборота, списания бонуса не уходят в дебет.
- Невозможно одновременно списать бонус и реальные средства не по правилу приоритета.
- `BonusGranted`, `BonusWagered`, `BonusExpired`, `BonusConverted`.
4) Оркестрации, саги и согласованность
Синхронно (CP): прием ставки и резерв средств — единый путь: `Bet.Place` → `Wallet.Reserve` (через доменную команду/оркестратор с дедлайном).
Асинхронно (EC): расчет ставки, начисление бонусов, аналитика — через события + outbox.
TCC-вариант: `TryReserve` (hold), `Confirm` (commit), `Cancel` (rollback) для денежных эффектов.
Идемпотентность: все команды несут `operation_id`, консьюмеры — `inbox`.
5) Антикоррупционные слои (ACL) и интеграции
Provider ACL: трансляция провайдерских событий `SpinResult`, `BonusWin` в внутренние `Round.Resulted`, `BonusWagered`. Схемы и версии — внутри ACL.
PSP ACL: нормализация статусов платежей, идемпотентность по `psp_tx_id`, перевод в `LedgerEntry`.
Compliance ACL: интеграции со списками санкций/PEP — во внешнем контексте; внутрь домена попадают только нормализованные `ScreeningUpdated`.
Правило: внешние словари/форматы не «просачиваются» внутрь ядра.
6) Проекции и Read Models
Player Profile Read Model: статусы KYC, лимиты, активные бонусы, свежие транзакции.
Balances Read Model: быстрые чтения для UI/маркетинга; источник — `Wallet` события.
Bet History Read Model: пагинация по датам/играм; источник — `BetPlaced/Settled`.
Compliance Reports: материализованные представления по тенанту/региону.
Все проекции — идемпотентные upsert’ы с версионированием и `as_of/freshness`.
7) Мульти-тенант и мульти-регион
Все ключевые сущности несут `tenant_id` и (при необходимости) `region`.
Границы данных: игрок, кошелек, ставки — «домашний» регион; кросс-региональные только агрегаты/отчеты.
Fairness/квоты: лимиты на команды/сек и редрайвы по тенантам.
Residency/комплаенс: персональные данные и проводки не покидают регион.
8) Выбор согласованности (PACELC) по контекстам
Wallet/Ledger — Strong/CP: линейризуемые проводки, кворум записей.
Bet acceptance — синхронное подтверждение (CP) + быстрые Read Models для UI.
Settlement/Bonus/Analytics — EC с детерминированным merge/компенсациями.
KYC/Compliance — может быть EC для статусов, но «блокирующие» правила применяются синхронно.
9) Доменные события: контракты и версия
Минимальный набор полей:json
{
"event_id": "uuid",
"event_type": "BetPlaced",
"occurred_at": "timestamp",
"tenant_id": "T123",
"aggregate_id": "BET-...-UUID",
"version": 7,
"payload": { "...domain fields..." },
"schema_version": "v3"
}
Правила:
- Back/forward-compat схем; эволюция через `schema_version`.
- `outbox` в транзакции с доменными изменениями; публикация батчами с backoff.
10) Пример потока «Ставка с бонусом» (словесная последовательность)
1. `Bet.Place` (команда) → проверка лимитов игрока и правил бонуса → `Wallet.Reserve(real+bonus_equiv, op_id)`
2. `BetPlaced` (событие) → Read Model обновляет «Открытые ставки»
3. Провайдер публикует результат → ACL → `Round.Resulted`
4. Оркестратор рассчитывает: `Bet.Settle(outcome,payout)` → `Wallet.Commit(op_id)` и, при выигрыше, `BonusWagered` → возможная конверсия бонуса в реальные.
5. `BetSettled` → проекции истории и балансов, отчетность.
11) Инварианты и политика тестирования
Ключевые инварианты:- Сумма всех `LedgerEntry` по кошельку равна балансу; нет отрицательных остатков.
- Нельзя принять ставку при активном self-exclusion/замороженном KYC-статусе.
- Вейджер может только уменьшаться и не качаться «в минус».
- Settlement не меняет статус уже финализированной ставки — только через `Void/Recalc` + компенсирующая проводка.
- Property-based тесты инвариантов кошелька и ставок.
- Контуры хаоса: задержки провайдера, отказы PSP, редрайвы outbox/DLQ.
- Контроль схем: миграции событий, backfill проекций.
12) Телеметрия и аудит
Метрики: p95/p99 на PlaceBet/Reserve/Commit, доля отказов по лимитам/KYC, DLQ rate, redrive success, lag проекций.
Трейсинг: спаны «команда→агрегат→outbox→консьюмер→проекция», теги `tenant_id`, `operation_id`, `saga_id`.
Аудит: неизменяемый журнал доменных действий, сопоставимый с регуляторными требованиями.
13) Схема хранения (упрощенно)
Wallet:
wallet(id, tenant_id, currency, balance, reserved, version)
ledger(id, wallet_id, amount, type, operation_id, occurred_at)
holds(id, wallet_id, amount, operation_id, expires_at, status)
Bet:
bet(id, tenant_id, player_id, amount, price, status, placed_at, settled_at, operation_id)
Bonus:
bonus_grant(id, tenant_id, player_id, amount, wager_left, status, expires_at)
Версионирование на агрегатах (`version`) защитит от lost update при конкурентной записи.
14) Пример API команд (псевдо)
http
POST /bets. place
{
"tenant_id":"T1",
"player_id":"P42",
"amount":"10. 00",
"price":"2. 1",
"operation_id":"op-uuid",
"context":{"game_id":"g777","channel":"web"}
}
→ 202 Accepted + BetPlaced
POST /wallets. reserve
{ "wallet_id":"W1", "amount":"10. 00", "operation_id":"op-uuid", "reason":"bet" }
→ 200 { "reserved_balance":"..." }
Все команды — с `operation_id` для идемпотентности, ответы — с `as_of`/`version`.
15) Безопасность и соответствие
RLS/ACL: все запросы — в контексте `tenant_id`, доступ по ролям.
PII-минимизация: отделение доменных событий от персональных данных; маскировка в DLQ/логах.
Регуляторные отчеты: проекции с неизменяемыми хэш-подписями по окнам времени.
16) Типичные ошибки
Сильная связность между контекстами (Wallet напрямую знает Bet/Bonus).
Dual-write в разные контексты без саг/outbox → рассогласование балансов и статусов.
Отсутствие идемпотентности команд и консьюмеров → дубли проводок/расчетов.
Протекание провайдерских контрактов в доменную модель (сложнее мигрировать).
Один «гигантский» агрегат (Player включает все) → блокировки, низкий throughput.
Нет явных инвариантов — их невозможно проверить и мониторить.
17) Быстрые рецепты
Старт: зафиксируйте Ubiquitous Language и контекстные границы; документируйте инварианты.
Деньги: Wallet/Ledger — CP, кворумные записи, TCC для внешних эффектов.
Ставки: синхронный прием + асинхронный расчет, все через события и outbox; идемпотентность везде.
Бонусы: отдельный агрегат с четким приоритетом списаний и вейджером.
Интеграции: всегда через ACL + схемы/версии; никаких «сырьевых» payload’ов в ядре.
Чтения: проекции/витрины на потребности продукта; SLA свежести + `as_of`.
Оперирование: метрики инвариантов, DLQ/редрайв плейбуки, rebuild витрин.
18) Чек-лист перед продом
- Определены bounded contexts и их контракты (команды/события).
- Агрегаты имеют явные инварианты, версионирование и идемпотентные команды.
- Денежные операции — через TCC/строгую транзакционность; аудит включен.
- Интеграции — через ACL с версионированием схем и тестами эволюции.
- Внедрены outbox/inbox, DLQ и безопасный редрайв.
- Проекции реализуют SLA свежести, есть метрики lag/staleness.
- Мульти-тенантные квоты/лимиты и data residency соблюдены.
- Наблюдаемость: трейсинг «команда→событие→проекция», алерты по инвариантам.
- Документация: язык домена, диаграммы контекстов, плейбуки инцидентов.
Заключение
DDD в iGaming-ядре — это дисциплина разделения сложности: четкие границы контекстов, агрегаты с инвариантами, события как источник правды, ACL для внешних интеграций и осознанный выбор согласованности. Такой подход делает платформу масштабируемой, надежной и соответствующей регуляциям, ускоряет разработку фич и снижает операционные риски — даже при быстром росте трафика, географий и продуктовой линейки.