Базы данных
System Design: платёжная система
Wise (TransferWise) обрабатывает $12 миллиардов международных переводов в месяц. Каждый перевод проходит через несколько банков в разных странах. Если деньги 'зависли' между системами - клиент в панике. Double-entry бухгалтерия + reconciliation позволяет Wise найти каждую потерянную копейку в течение минут.
- **Stripe**: idempotency keys обязательны для всех Payment Intents API вызовов - это защищает от retry storms
- **Brex**: event sourcing на Kafka - баланс корпоративных карт это sum всех ledger events, не хранимое поле
- **Wise**: near-real-time reconciliation каждые 15 минут с банками-партнёрами в 80+ странах
Double-entry бухгалтерия в БД
Double-entry accounting - каждая финансовая транзакция записывается минимум дважды: дебет одного счёта = кредит другого. Сумма всех дебетов всегда равна сумме всех кредитов. Это математический инвариант, который позволяет обнаружить любую ошибку или потерю данных. Stripe, Brex, Wise используют double-entry как фундамент.
Никогда нельзя хранить деньги в FLOAT или DOUBLE - IEEE 754 float не может точно представить большинство десятичных дробей. 0.1 + 0.2 = 0.30000000000000004 в float. Используйте BIGINT (в центах/копейках) или DECIMAL(19, 4) - это стандарт в финтехе.
Почему нельзя хранить денежные суммы в столбце типа FLOAT?
Idempotency keys
Платёжные запросы могут быть отправлены повторно: таймаут клиента, retry при сетевой ошибке, дублирующий webhook. Idempotency key - уникальный клиентский ключ, который гарантирует что повторный запрос с тем же ключом даёт тот же результат без повторной обработки. Stripe требует Idempotency-Key заголовок для всех мутирующих API вызовов.
Stripe хранит idempotency keys 24 часа. Wise (TransferWise) использует 2-phase commit с idempotency для cross-border платежей через несколько банков. PayPal внедрил idempotency после инцидента 2013 года, когда retry шторм создал двойные списания на $170M.
Клиент отправил запрос на перевод $50 и получил таймаут (нет ответа). Что произойдёт при повторном запросе с тем же idempotency key?
Reconciliation: сверка данных
Reconciliation - процесс сверки внутренних записей с внешними системами (банк, платёжный провайдер, карточные сети). Даже с idempotency и транзакциями возможно расхождение: банк принял платёж, но ответ потерян; провайдер выполнил возврат без уведомления. Reconciliation обнаруживает и устраняет несоответствия.
Wise проводит reconciliation в режиме near-real-time (каждые 15 минут) для критических коридоров. Stripe reconciles 24/7 с Visa/Mastercard через ISO 8583 файлы. Typical reconciliation variance (discrepancy rate) у финтех компаний: <0.001% от объёма - за этим следят как за SLA.
Reconciliation показал транзакцию в банке, которой нет во внутренней системе. Что это означает?
Audit Trail и неизменяемость
Audit trail - полная неизменяемая история всех изменений в финансовой системе. Финансовые регуляторы (PCI DSS, SOX) требуют хранить историю транзакций минимум 7 лет. Данные не должны изменяться - только добавляться. Event sourcing - архитектурный паттерн где состояние системы = replay всех событий.
Amazon DynamoDB Streams и AWS Kinesis Data Firehose часто используются для capture изменений в operational БД и записи в immutable audit store в S3. Brex (финтех) использует event sourcing на Kafka: все изменения счетов - события, баланс = sum всех событий.
Для хранения транзакций достаточно одной таблицы с текущим балансом и обновлять его при каждой операции
Double-entry ledger с immutable events гарантирует что каждая копейка учтена и никогда не потеряна; баланс - это лишь производное от истории событий
Если обновлять только баланс - при любом сбое теряется информация о том что именно произошло. Можно восстановить итог, но не причину. Double-entry + event sourcing позволяет reconstruct любое состояние на любой момент времени и доказать его регулятору.
Почему в audit trail таблице нельзя делать UPDATE или DELETE строк?
Итоги
- **Double-entry** - каждая транзакция = deb + cred, sum всегда = 0, математический инвариант целостности
- **Idempotency keys** - клиентский UUID предотвращает двойную обработку retry запросов; ответ кэшируется на стороне сервера
- **Audit trail** - append-only ledger с row-level security; баланс = производная от истории, не хранимое поле
Связанные темы
Платёжные системы опираются на несколько критических концепций:
- ACID транзакции — Double-entry требует atomic commit двух ledger записей - без ACID один из двух INSERT может упасть
- Репликация — Платёжные БД используют synchronous replication - нельзя потерять подтверждённую транзакцию при failover
- Мониторинг и алертинг — Reconciliation variance >0.001% должен вызывать алерт немедленно - это SLA критической системы
Вопросы для размышления
- Как правильно обработать частичный возврат (refund) через double-entry? Сколько ledger entries создаётся для возврата $30 из $100 покупки?
- Система обнаружила reconciliation mismatch: у нас $1000 платежей, в банке $999.99. Алгоритм действий для расследования?
- Идемпотентность - достаточное условие для безопасности платежей или нужны дополнительные гарантии? Что не защищает idempotency key?