Данные и деньги: собери корректный двигатель денег
Практический проект: собери небольшой сервис кошелька/ledger, что переживает ретраи и конкуренцию — двойная запись, клиентские ключи идемпотентности, атомарный перевод против потерянного обновления и проход сверки — доказанный адверсариальными тестами.
Прочитать, что ключи идемпотентности и двойная запись предотвращают двойные списания и потерянные обновления, — не то же, что собрать сервис, что переживает шторм ретраев и тысячу конкурентных переводов, не потеряв ни цента. Собери небольшой двигатель денег, затем докажи его корректность единственным способом, что считается: адверсариальными тестами, что кидают в него дублирующие запросы и конкурентные дебеты и проверяют, что книги всё ещё сходятся.
Этот проект делает юнит операционным: ты реализуешь два несущих паттерна — идемпотентность и двойную запись — побьёшь потерянное обновление атомарным переводом, а затем атакуешь свою же систему дубликатами и конкуренцией, чтобы доказать инвариант, что книги всегда сходятся.
Собери небольшой сервис кошелька/ledger (любой язык + реальная транзакционная БД), что двигает деньги между счетами корректно под ретраями и конкуренцией. Он должен реализовать append-only ledger двойной записи, принимать клиентские ключи идемпотентности, выполнять атомарные переводы, что не могут списать дважды или потерять обновление, и включать проход сверки, что проверяет схождение книг — затем доказать всё это адверсариальными тестами.
- Append-only ledger, где балансы вычисляются суммированием неизменяемых записей, со скриптом сидинга и запросом, что печатает баланс любого счёта и его историю записей.
- Перевод, что атомарен (обе записи или ни одной) и идемпотентен (повторный ключ возвращает исходный результат без второго движения; конфликтующий ключ отклоняется).
- Проходящий тест шторма ретраев: N конкурентных идентичных запросов производят записи ровно на один перевод и единственную смену баланса.
- Проходящий тест конкурентных дебетов: много переоверкоммиченных конкурентных переводов оставляют счёт неотрицательным и глобальный инвариант дебеты = кредиты целым.
- Команда сверки, что пересчитывает балансы из записей и репортит книги как сбалансированные (и пометила бы внесённый дисбаланс), плюс заметка дизайна, объясняющая выбор изоляции/блокировки и один отказ, что сверка ловит.
- Добавь межшардовый перевод как saga: раздели счета на две БД, реализуй дебет→кредит с компенсирующим действием, смоделируй in-flight pending-состояние и покажи, как сверка исцеляет перевод, что ты намеренно прервал между двумя ногами.
- Добавь снимок/материализованный баланс и обслуживай чтения из него (снимок + свёртка хвоста свежих записей), затем докажи, что снимок всегда равен полной пересвёртке — оптимизация чтения event-sourcing без потери проверяемости.
- Добавь внешний 'PSP'-заглушку, чей ответ можешь дропнуть/задержать, поставь списание за ключом идемпотентности и покажи, что потерянный ответ + ретрай клиента не списывает дважды, пока дневная сверка с отчётом PSP всё же ловит расхождение.
- Нагрузь горячий счёт: сделай один счёт целью большинства переводов и измерь, как твой выбор блокировки его сериализует; сравни пессимистичную блокировку против атомарной условной записи против оптимистичной/CAS при высокой конкуренции и отрепорти пропускную способность и частоту ретраев.
- 01Почему выводить балансы суммированием неизменяемых записей, а не хранить изменяемое поле баланса?
- 02Как два адверсариальных теста доказывают два ядровых инварианта?
- 03Что проход сверки ловит, чего один горячий путь не может?
Этот проект превращает два несущих паттерна юнита в рабочий код. Ты собираешь append-only ledger двойной записи, где каждый перевод — две неизменяемые записи в сумме ноль, а баланс — выводимая сумма, никогда изменяемое поле — так что деньги сохраняются по построению, а книги проверяемы. Перевод — это одна атомарная транзакция (обе ноги или ни одной), охраняемая клиентским ключом идемпотентности (атомарный check-and-store под уникальным ограничением), а потерянное обновление побеждается атомарной условной проверкой, блокировкой строки или serializable-транзакцией. Затем ты атакуешь свою же систему: шторм ретраев (N идентичных конкурентных запросов должны двинуть деньги ровно раз) доказывает идемпотентность, а конкурентные переоверкоммиченные дебеты (счёт никогда не должен уйти в минус, дебеты должны всё ещё равняться кредитам) доказывают, что потерянное обновление закрыто. Наконец проход сверки независимо пересуммирует записи и утверждает глобальный инвариант — слой, что ловит потерянные ответы, прерванные saga и дрейф, что горячий путь не видит. Инженер, что собрал это однажды, перестаёт доверять одному ответу как истине о деньгах и начинает доверять сверенному append-only ledger — и знает, с тестами в доказательство, что ретраи и конкуренция не заставят книги лгать.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.