Платежная система
В этой главе мы спроектируем payment system. За последние годы e-commerce стремительно выросла по всему миру. За каждой транзакцией, которая происходит на такой платформе, незаметно работает payment system. Надежная, scalable и flexible payment system критически важна.
Что такое payment system? Согласно Wikipedia, “payment system is any system used to settle financial transactions through the transfer of monetary value. This includes the institutions, instruments, people, rules, procedures, standards, and technologies that make its exchange possible” [1].
На поверхности payment system кажется понятной, но для многих developers она выглядит пугающе. Даже небольшая ошибка может привести к серьезным финансовым потерям и подорвать доверие users. Но бояться не стоит: в этой главе мы разложим payment systems по полочкам.
Шаг 1 — Понять задачу и определить рамки дизайна
Под выражением payment system разные люди могут понимать совершенно разные вещи. Кто-то думает о digital wallet вроде Apple Pay или Google Pay. Кто-то — о backend system, обрабатывающей payments, как PayPal или Stripe. Поэтому в начале interview очень важно точно определить requirements. Вот какие вопросы можно задать interviewer-у:
Кандидат: Какого рода payment system мы строим?
Интервьюер: Представим, что ты строишь payment backend для e-commerce application вроде
Amazon.com. Когда customer размещает order на Amazon.com, payment system обрабатывает все, что
связано с движением денег.
Кандидат: Какие payment options поддерживаются? Credit cards, PayPal, bank cards и т. д.?
Интервьюер: В реальной жизни payment system должна поддерживать все эти варианты. Но в рамках
этого interview давай использовать payment по credit card как пример.
Кандидат: Мы сами обрабатываем credit card payment processing?
Интервьюер: Нет, мы используем third-party payment processors, например Stripe, Braintree,
Square и т. д.
Кандидат: Мы храним credit card data в нашей system?
Интервьюер: Нет. Из-за крайне высоких требований к security и compliance мы не храним card
numbers напрямую в нашей system. Мы полагаемся на third-party payment processors для работы с
sensitive credit card data.
Кандидат: Приложение глобальное? Нужно ли поддерживать разные currencies и international
payments?
Интервьюер: Отличный вопрос. Да, application глобальное, но в этом interview предположим, что
используется только одна currency.
Кандидат: Сколько payment transactions в день?
Интервьюер: 1 миллион transactions в день.
Кандидат: Нужно ли поддерживать pay-out flow, который e-commerce site вроде Amazon использует
для выплат sellers раз в месяц?
Интервьюер: Да, это тоже нужно поддержать.
Кандидат: Кажется, я собрал все requirements. Есть ли еще что-то, на что стоит обратить
внимание?
Интервьюер: Да. Payment system взаимодействует со множеством internal services
(accounting, analytics и т. д.) и external services (payment service providers). Когда какой-то
service падает, между системами могут возникать inconsistent states. Поэтому нам нужен процесс
reconciliation, который будет находить и исправлять несогласованности. Это тоже requirement.
С этими вопросами мы получаем четкую картину и functional, и non-functional requirements. В этом interview мы сосредоточимся на проектировании payment system, поддерживающей следующее.
Functional requirements
-
Pay-in flow: payment system получает деньги от customers от имени sellers.
-
Pay-out flow: payment system отправляет деньги sellers по всему миру.
Нефункциональные требования
-
Reliability и fault tolerance. Failed payments нужно обрабатывать очень аккуратно.
-
Нужен reconciliation process между internal services (payment systems, accounting systems) и external services (payment service providers). Этот процесс асинхронно проверяет, что payment information во всех этих systems согласована.
Back-of-the-envelope estimation
System должна обрабатывать 1 миллион transactions в день, то есть
1,000,000 transactions / 10^5 seconds = 10 transactions per second (TPS). 10 TPS — не слишком
большое число для типичной database. Значит, фокус этого system design interview не на высоком
throughput, а на корректной обработке payment transactions.
Шаг 2 — Предложить высокоуровневый дизайн и согласовать его
На high level payment flow делится на два шага, отражающие движение денег:
-
Pay-in flow
-
Pay-out flow
Возьмем e-commerce site Amazon как пример. После того как buyer размещает order, деньги поступают на bank account Amazon — это pay-in flow. Хотя деньги находятся на bank account Amazon, Amazon не владеет всей суммой. Существенная часть принадлежит seller-у, а Amazon лишь выступает хранителем денег за комиссию. Позже, когда products доставлены и деньги можно release, оставшийся после fees balance переводится с bank account Amazon на bank account seller-а. Это и есть pay-out flow. Упрощенные pay-in и pay-out flows показаны на рисунке 1.
Рисунок 1 Упрощенные pay-in и pay-out flows
Pay-in flow
High-level design pay-in flow показан на рисунке 2. Разберем каждый component этой system.

Рисунок 2 Pay-in flow
Payment service
Payment service принимает payment events от users и координирует payment process. Обычно первое, что она делает, — risk check: проверка compliance с такими регуляторными требованиями, как AML/CFT [2], а также поиск признаков criminal activity, например money laundering или financing of terrorism. Payment service обрабатывает только те payments, которые прошли этот risk check. Обычно risk check service использует third-party provider, потому что эта область очень сложная и узкоспециализированная.
Payment executor
Payment executor исполняет отдельный payment order через Payment Service Provider (PSP). Один payment event может содержать несколько payment orders.
Payment Service Provider (PSP)
PSP перемещает деньги со счета A на счет B. В этом упрощенном примере PSP списывает деньги с credit card account buyer-а.
Card schemes
Card schemes — это организации, обрабатывающие операции по credit cards. Самые известные card schemes — Visa, MasterCard, Discovery и т. д. Экосистема card schemes довольно сложная [3].
Ledger
Ledger хранит финансовую запись payment transaction. Например, когда user платит seller-у $1, мы записываем это как debit $1 у user-а и credit $1 у seller-а. Ledger system очень важна для post-payment analysis, например для расчета total revenue e-commerce website или прогноза future revenue.
Wallet
Wallet хранит account balance merchant-а. Она также может хранить, сколько всего заплатил тот или иной user.
Как показано на рисунке 2, типичный pay-in flow работает так:
-
Когда user нажимает кнопку “place order”, генерируется payment event и отправляется в payment service.
-
Payment service сохраняет payment event в database.
-
Иногда один payment event может содержать несколько payment orders. Например, можно выбрать products от нескольких sellers в одном checkout process. Если e-commerce website разбивает такой checkout на несколько payment orders, payment service вызывает payment executor для каждого payment order.
-
Payment executor сохраняет payment order в database.
-
Payment executor вызывает внешний PSP, чтобы обработать credit card payment.
-
После того как payment executor успешно обработал payment, payment service обновляет wallet, чтобы зафиксировать, сколько денег теперь у конкретного seller-а.
-
Wallet server сохраняет обновленную balance information в database.
-
После того как wallet service успешно обновила balance information seller-а, payment service вызывает ledger, чтобы обновить его.
-
Ledger service дописывает новую ledger information в database.
APIs для payment service
Для payment service мы используем RESTful API design convention.
POST /v1/payments
Этот endpoint исполняет payment event. Как уже говорилось выше, один payment event может содержать несколько payment orders. Request parameters перечислены ниже:
| Field | Description | Type |
|---|---|---|
| buyer_info | Информация о buyer-е | json |
| checkout_id | Глобально уникальный ID этого checkout-а | string |
| credit_card_info | Это может быть encrypted credit card information или payment token. Значение зависит от PSP. | json |
| payment_orders | Список payment orders | list |
Таблица 1 Параметры API request-а (исполнение payment event)
payment_orders выглядят так:
| Field | Description | Type |
|---|---|---|
| seller_account | Какой seller получит деньги | string |
| amount | Transaction amount для order-а | string |
| currency | Currency для order-а | string (ISO 4217 [4]) |
| payment_order_id | Глобально уникальный ID этого payment-а | string |
Таблица 2 payment_orders
Обратите внимание, что payment_order_id глобально уникален. Когда payment executor отправляет
payment request во внешний PSP, payment_order_id используется PSP как deduplication ID, также
называемый idempotency key.
Вы могли заметить, что data type у поля amount — string, а не double. Double здесь —
плохой выбор по двум причинам:
-
Разные protocols, software и hardware могут поддерживать разную numeric precision при serialization и deserialization. Из-за этого легко получить нежелательные rounding errors.
-
Число может быть очень большим (например, GDP Японии за 2020 календарный год — около
5x10^14yen), а может быть очень маленьким (например, один satoshi у Bitcoin —10^-8).
Рекомендуется хранить numbers в формате string как при передаче, так и при storage. В number их
стоит преобразовывать только в момент display или calculation.
GET /v1/payments/{:id}
Этот endpoint возвращает execution status одного payment order по payment_order_id.
Упомянутый выше payment API похож на API некоторых известных PSP. Если интересно посмотреть на payment APIs шире, стоит заглянуть в Stripe API documentation [5].
Data model для payment service
Для payment service нам нужны две tables: payment_event и payment_order. Когда мы выбираем
storage solution для payment system, performance обычно не самый важный фактор. Вместо этого мы
смотрим на следующее:
-
Проверенная стабильность. Использовалась ли storage system большими financial firms на протяжении многих лет (например, более 5 лет) с положительным feedback.
-
Богатство supporting tools, например monitoring и investigation tools.
-
Зрелость рынка DBA. Возможность нанять experienced DBAs — очень важный фактор.
Обычно мы предпочитаем traditional relational database с ACID transaction support, а не NoSQL/NewSQL.
Таблица payment_event содержит детальную информацию о payment event. Она выглядит так:
| Name | Type |
|---|---|
| checkout_id | string PK |
| buyer_info | string |
| seller_info | string |
| credit_card_info | зависит от card provider |
| is_payment_done | boolean |
Таблица 3 Payment event
Таблица payment_order хранит execution status каждого payment order. Она выглядит так:
| Name | Type |
|---|---|
| payment_order_id | String PK |
| buyer_account | string |
| amount | string |
| currency | string |
| checkout_id | string FK |
| payment_order_status | string |
| ledger_updated | boolean |
| wallet_updated | boolean |
Таблица 4 Payment order
Прежде чем углубляться в эти tables, посмотрим на важный background context.
-
checkout_id— это foreign key. Один checkout создает payment event, который может содержать несколько payment orders. -
Когда мы вызываем third-party PSP, чтобы списать деньги с credit card buyer-а, деньги переводятся не напрямую seller-у. Вместо этого они переводятся на bank account e-commerce website. Этот процесс называется pay-in. Когда выполняется условие для pay-out, например products доставлены, seller инициирует pay-out. Только тогда деньги переводятся с bank account e-commerce website на bank account seller-а. Поэтому во время pay-in flow нам нужна только card information buyer-а, а bank account information seller-а не нужна.
В таблице payment_order (таблица 4) payment_order_status — это enum, хранящий execution status
payment order. Возможные значения: NOT_STARTED, EXECUTING, SUCCESS, FAILED. Логика
обновления такая:
-
Начальный статус
payment_order_status—NOT_STARTED. -
Когда payment service отправляет payment order в payment executor,
payment_order_statusстановитсяEXECUTING. -
Payment service обновляет
payment_order_statusнаSUCCESSилиFAILEDв зависимости от ответа payment executor.
Как только payment_order_status становится SUCCESS, payment service вызывает wallet service,
чтобы обновить seller balance, и выставляет wallet_updated = TRUE. Здесь для упрощения дизайна
мы предполагаем, что wallet update всегда проходит успешно.
После этого следующим шагом payment service вызывает ledger service, чтобы обновить ledger database
и выставить ledger_updated = TRUE.
Когда все payment orders под одним и тем же checkout_id успешно обработаны, payment service
обновляет is_payment_done = TRUE в таблице payment_event. Обычно scheduled job запускается с
фиксированным interval и мониторит статус in-flight payment orders. Если payment order не
завершается в пределах заданного threshold, job отправляет alert, чтобы engineers могли
расследовать ситуацию.
Double-entry ledger system
В ledger system есть один очень важный design principle: принцип double-entry (double-entry accounting/bookkeeping [6]). Double-entry system фундаментальна для любой payment system и критически важна для точного bookkeeping. Она записывает каждую payment transaction в два разных ledger account с одинаковой суммой. Один account дебетуется, другой кредитуется на ту же сумму (таблица 5).
| Account | Debit | Credit |
|---|---|---|
| buyer | $1 | |
| seller | $1 |
Таблица 5 Double-entry system
Double-entry system утверждает, что сумма всех transaction entries должна быть равна 0. Если один cent потерян, значит кто-то другой приобрел этот cent. Такой подход дает end-to-end traceability и обеспечивает consistency на протяжении всего payment cycle. Если хотите глубже понять реализацию double-entry system, посмотрите engineering blog Square про immutable double-entry accounting database service [7].
Hosted payment page
Большинство компаний предпочитают не хранить credit card information у себя внутри, потому что в этом случае приходится соответствовать сложным regulations вроде Payment Card Industry Data Security Standard (PCI DSS) [8] в США. Чтобы не работать с credit card information напрямую, компании используют hosted credit card pages, которые предоставляет PSP. Для websites это widget или iframe, для mobile applications — часто готовая page из payment SDK. На рисунке 3 показан пример checkout experience с PayPal integration. Ключевая идея в том, что PSP предоставляет hosted payment page, которая собирает customer card information напрямую, не полагаясь на нашу payment service.

Рисунок 3 Hosted pay with PayPal page
Pay-out flow
Components pay-out flow очень похожи на components pay-in flow. Отличие в том, что вместо PSP, который переводит деньги с credit card buyer-а на bank account e-commerce website, pay-out flow использует third-party pay-out provider, чтобы перевести деньги с bank account e-commerce website на bank account seller-а.
Обычно payment system использует third-party account payable providers вроде Tipalti [9] для обработки pay-outs. У pay-out есть не меньше bookkeeping и regulatory requirements, чем у pay-in.
Шаг 3 — Детальное проектирование
В этом разделе мы сосредоточимся на том, как сделать system быстрее, надежнее и безопаснее. В distributed system ошибки и сбои не просто возможны — они неизбежны и происходят постоянно. Например, что будет, если customer нажмет кнопку “pay” несколько раз? Спишем ли мы деньги несколько раз? Как обрабатывать payment failures из-за плохого network connection? Ниже разберем несколько важных тем.
-
PSP integration
-
Reconciliation
-
Handling payment processing delays
-
Communication among internal services
-
Handling failed payments
-
Exactly-once delivery
-
Consistency
-
Security
PSP integration
Если payment system умеет напрямую подключаться к banks или card schemes вроде Visa или MasterCard, payment можно делать без PSP. Но такие прямые integrations встречаются редко и сильно специализированы. Обычно они оправданы только для очень больших компаний. Для большинства компаний payment system интегрируется с PSP одним из двух способов:
-
Если компания умеет безопасно хранить sensitive payment information и сознательно выбирает этот путь, PSP можно интегрировать через API. Компания сама отвечает за разработку payment web pages, сбор и хранение sensitive payment information. PSP отвечает за подключение к banks или card schemes.
-
Если компания решает не хранить sensitive payment information из-за сложных regulations и security concerns, PSP предоставляет hosted payment page для сбора card payment details и безопасного хранения их у себя. Именно этот подход выбирает большинство компаний.
Используем рисунок 4, чтобы подробно объяснить, как работает hosted payment page.

Рисунок 4 Hosted payment flow
Для простоты на рисунке 4 мы опустили payment executor, ledger и wallet. Payment service оркестрирует весь payment process.
-
User нажимает кнопку “checkout” в client browser. Client вызывает payment service, передавая payment order information.
-
Получив payment order information, payment service отправляет в PSP payment registration request. Такой request содержит payment information: amount, currency, expiration date payment request-а и redirect URL. Так как payment order должен быть зарегистрирован только один раз, используется UUID field для exactly-once registration. Этот UUID также называется nonce [10]. Обычно этот UUID — ID payment order.
-
PSP возвращает token обратно в payment service. Token — это UUID на стороне PSP, который уникально идентифицирует payment registration. Позже по этому token можно посмотреть payment registration и execution status.
-
Payment service сохраняет token в database до того, как вызвать PSP-hosted payment page.
-
После того как token записан в storage, client показывает PSP-hosted payment page. Mobile applications обычно используют для этого SDK integration от PSP. Здесь в качестве примера используется Stripe web integration (рисунок 5). Stripe предоставляет JavaScript library, которая отображает payment UI, собирает sensitive payment information и напрямую вызывает PSP, чтобы завершить payment. Sensitive payment information собирает сам Stripe. До нашей payment system она не доходит. Hosted payment page обычно требует две части information:
-
Token, который мы получили на шаге 4. JavaScript code PSP использует token, чтобы получить из backend PSP детальную информацию о payment request. Один из важных параметров — сколько денег нужно списать.
-
Второй важный параметр — redirect URL. Это URL web page, на которую перенаправляют после завершения payment. Когда JavaScript PSP заканчивает payment, browser редиректится на redirect URL. Обычно это web page e-commerce website, показывающая статус checkout-а. Обратите внимание, что redirect URL отличается от webhook [11] URL из шага 9.
-

Рисунок 5 Hosted payment page by Stripe
-
User заполняет payment details на web page PSP: credit card number, holder name, expiration date и т. д., затем нажимает кнопку pay. PSP начинает payment processing.
-
PSP возвращает payment status.
-
Web page перенаправляется на redirect URL. Payment status, полученный на шаге 7, обычно добавляется к URL. Например, полный redirect URL может выглядеть так [12]:
https://your-company.com/?tokenID=JIOUIQ123NSF&payResult=X324FSa -
Асинхронно PSP вызывает payment service с payment status через webhook. Webhook — это URL на стороне payment system, заранее зарегистрированный в PSP при первоначальной настройке integration. Когда payment system получает payment events через webhook, она извлекает payment status и обновляет поле
payment_order_statusв таблицеPayment Order.
До сих пор мы описывали happy path hosted payment page. В реальности network connection может быть ненадежным, и любой из 9 шагов выше может завершиться неудачей. Есть ли системный способ работать с failure cases? Да: reconciliation.
Reconciliation
Когда components system общаются асинхронно, нет никакой гарантии, что message будет доставлено или что response вообще вернется. Для payment business это очень типичная ситуация, потому что асинхронное взаимодействие часто используется для повышения performance. External systems, такие как PSP и banks, тоже предпочитают asynchronous communication. Как же в этом случае обеспечивать correctness?
Ответ — reconciliation. Это практика, при которой состояния связанных services периодически сравниваются, чтобы убедиться, что они совпадают. Обычно именно reconciliation служит последней линией обороны payment system.
Каждую ночь PSP или banks отправляют своим clients settlement file. Settlement file содержит balance bank account-а и все transactions, произошедшие по этому счету за день. Reconciliation system парсит settlement file и сравнивает его содержимое с ledger system. На рисунке 6 показано, где именно reconciliation process находится в общей system.

Рисунок 6 Reconciliation
Reconciliation также используется для проверки внутренней consistency payment system. Например, состояния в ledger и wallet могут разойтись, и reconciliation system позволит обнаружить это расхождение.
Чтобы исправлять mismatches, найденные при reconciliation, обычно привлекают finance team для manual adjustments. Обычно mismatches и adjustments делят на три категории:
-
Mismatch можно классифицировать, и adjustment можно автоматизировать. В этом случае мы знаем причину mismatch, знаем, как ее исправить, и написание automation экономически оправдано. Engineers могут автоматизировать и classification mismatch, и adjustment.
-
Mismatch можно классифицировать, но автоматизировать adjustment нельзя. В этом случае мы понимаем причину mismatch и способ исправления, но стоимость написания auto-adjustment program слишком высока. Такой mismatch помещается в job queue, и finance team исправляет его вручную.
-
Mismatch нельзя классифицировать. В этом случае мы не понимаем, почему mismatch возник. Такой mismatch попадает в специальную job queue. Finance team расследует его вручную.
Handling payment processing delays
Как уже обсуждалось, end-to-end payment request проходит через множество components и затрагивает как internal, так и external parties. Хотя в большинстве случаев payment request завершается за секунды, бывают ситуации, когда запрос зависает и завершается или отклоняется только через часы или даже дни. Вот несколько примеров:
-
PSP считает payment request high risk и требует human review.
-
Credit card требует дополнительной защиты, например 3D Secure Authentication [13], где card holder должен предоставить дополнительную information для подтверждения purchase.
Payment service должна уметь работать с такими long-running payment requests. Если buy page hosted у внешнего PSP, что сегодня очень распространено, PSP обычно обрабатывает такие длительные requests так:
-
PSP возвращает client-у pending status. Client показывает его user-у. Кроме того, client предоставляет page, где customer может проверить текущий payment status.
-
PSP отслеживает pending payment от нашего имени и уведомляет payment service о любом изменении status через webhook, который payment service зарегистрировала в PSP.
Когда payment request наконец завершается, PSP вызывает зарегистрированный webhook, о котором шла речь выше. Payment service обновляет internal system и завершает shipment customer-у.
Альтернативный вариант: вместо webhook некоторые PSP перекладывают обязанность по отслеживанию status на payment service и ожидают, что та будет периодически poll-ить PSP по pending payment requests.
Communication among internal services
Есть два основных communication patterns, которые internal services используют для взаимодействия: synchronous и asynchronous. Разберем оба.
Synchronous communication
Synchronous communication вроде HTTP хорошо подходит для small-scale systems, но по мере роста scale ее недостатки становятся очевидны. Она создает длинную request/response chain, зависящую от многих services. Минусы этого подхода:
-
Низкая performance. Если хотя бы один service в цепочке работает плохо, страдает вся system.
-
Плохая failure isolation. Если PSP или любой другой service падает, client перестает получать response.
-
Tight coupling. Sender request-а должен знать recipient-а.
-
Сложно масштабировать. Если не использовать queue как buffer, system трудно подготовить к резкому всплеску traffic.
Asynchronous communication
Asynchronous communication можно разделить на две категории:
- Single receiver: каждый request (message) обрабатывается одним receiver-ом или service-ом.
Обычно это реализуют через shared message queue. Queue может иметь несколько subscribers, но как
только message обработано, оно удаляется из queue. Посмотрим на конкретный пример. На рисунке 7
service A и service B обе подписаны на shared message queue. Когда
m1иm2consumed service A и service B соответственно, оба messages удаляются из queue, как показано на рисунке 8.
Рисунок 7 Message queue
Рисунок 8 Single receiver for each message
- Multiple receivers: каждый request (message) обрабатывается несколькими receivers или services. Здесь хорошо подходит Kafka. Когда consumers получают messages, они не удаляются из Kafka. Одно и то же message могут обработать разные services. Эта модель хорошо ложится на payment system, где один и тот же request может запускать несколько side effects: push notifications, financial reporting, analytics и т. д. Пример показан на рисунке 9. Payment events публикуются в Kafka и потребляются разными services, например payment system, analytics service и billing service.
Рисунок 9 Multiple receivers for the same message
В целом synchronous communication проще по design, но не позволяет services быть автономными. Чем сложнее dependency graph, тем сильнее страдает общая performance. Asynchronous communication жертвует design simplicity и consistency ради scalability и resilience to failure. Для large-scale payment system со сложной business logic и большим числом third-party dependencies asynchronous communication — лучший выбор.
Handling failed payments
Любая payment system должна уметь работать с failed transactions. Reliability и fault tolerance — ключевые requirements. Рассмотрим несколько techniques, которые помогают решать эти задачи.
Tracking payment state
Наличие однозначного payment state на любом этапе payment cycle критически важно. Когда происходит failure, мы можем определить текущее состояние payment transaction и понять, нужен ли retry или refund. Payment state можно persist-ить в append-only database table.
Retry queue и dead letter queue
Чтобы аккуратно обрабатывать failures, используем retry queue и dead letter queue, как показано на рисунке 10.
-
Retry queue: retryable errors, например transient errors, направляются в retry queue.
-
Dead letter queue [14]: если message repeatedly fails, в конце концов оно попадает в dead letter queue. Dead letter queue полезна для debugging и изоляции problematic messages, чтобы можно было изучить, почему они не были успешно обработаны.
Рисунок 10 Handle failed payments
-
Проверяем, является ли failure retryable.
1a. Retryable failures направляются в retry queue.
1b. Non-retryable failures, например invalid input, сохраняются в database.
-
Payment system читает events из retry queue и повторяет failed payment transactions.
-
Если payment transaction снова падает:
3a. Если retry count не превышает threshold, event снова отправляется в retry queue.
3b. Если retry count превышает threshold, event кладется в dead letter queue. Такие failed events, возможно, придется расследовать вручную.
Если вам интересен реальный пример использования таких queues, посмотрите на payment system Uber, которая использует Kafka для обеспечения reliability и fault tolerance [16].
Exactly-once delivery
Одна из самых серьезных проблем payment system — двойное списание с customer-а. В нашем design важно гарантировать, что payment system исполняет payment order exactly-once [16].
На первый взгляд exactly-once delivery выглядит очень сложной задачей, но если разбить проблему на две части, она становится куда проще. Математически операция выполняется exactly-once, если:
-
Она выполняется at-least-once.
-
И одновременно выполняется at-most-once.
Ниже разберем, как реализовать at-least-once через retry, а at-most-once — через idempotency check.
Retry
Иногда payment transaction приходится повторять из-за network errors или timeout. Именно retry дает гарантию at-least-once. Например, на рисунке 11 client пытается выполнить payment на $10, но request несколько раз падает из-за плохого network connection. В этом примере сеть в итоге восстановилась, и request прошел с четвертой попытки.
Рисунок 11 Retry
Очень важно правильно выбрать intervals между retries. Вот несколько типичных retry strategies.
-
Immediate retry: client немедленно повторно отправляет request.
-
Fixed intervals: между моментом failed payment и новой retry attempt выдерживается фиксированное время.
-
Incremental intervals: для первого retry client ждет недолго, а затем постепенно увеличивает ожидание для последующих retries.
-
Exponential backoff [17]: после каждого failed retry время ожидания удваивается. Например, после первого failure мы retry через 1 second; после второго ждем 2 seconds; после третьего — 4 seconds.
-
Cancel: client может отменить request. Это обычная практика, когда failure permanent или repeated requests вряд ли будут успешны.
Выбрать правильную retry strategy сложно. Универсального решения не существует. Как общее правило,
используйте exponential backoff, если network issue вряд ли будет устранена быстро. Слишком
агрессивные retries тратят computing resources впустую и могут перегрузить service. Хорошая
практика — возвращать error code вместе с заголовком Retry-After.
Но у retry есть потенциальная проблема: double payments. Рассмотрим два scenarios.
Scenario 1: Payment system интегрирована с PSP через hosted payment page, и client дважды нажимает кнопку pay.
Scenario 2: Payment успешно обработан PSP, но response не дошел до нашей payment system из-за network errors. User снова нажимает кнопку “pay” или client автоматически retry-ит payment.
Чтобы избежать double payment, payment должен исполняться at-most-once. Эту гарантию at-most-once также называют idempotency.
Idempotency
Idempotency — ключ к обеспечению гарантии at-most-once. Согласно Wikipedia, “idempotence is the property of certain operations in mathematics and computer science whereby they can be applied multiple times without changing the result beyond the initial application” [18]. С точки зрения API idempotency означает, что client может многократно делать один и тот же call и получать один и тот же result.
Для communication между clients (web и mobile applications) и servers idempotency key обычно
представляет собой unique value, которую client генерирует и которая истекает через некоторое
время. В качестве idempotency key часто используют UUID; такой подход рекомендуют многие tech
компании, включая Stripe [19] и PayPal [20]. Чтобы сделать payment request idempotent,
idempotency key добавляют в HTTP header: <idempotency-key: key_value>.
Теперь, когда основы idempotency понятны, посмотрим, как она помогает решить проблемы double payment из scenarios выше.
Scenario 1: что если customer быстро дважды нажал кнопку “pay”?
На рисунке 12, когда user нажимает “pay”, вместе с HTTP request в payment system отправляется idempotency key. На e-commerce website idempotency key обычно является ID shopping cart прямо перед checkout.
Для второго request это будет трактоваться как retry, потому что payment system уже видела эту idempotency key. Когда в request header приходит ранее использованный idempotency key, payment system возвращает latest status предыдущего request-а.
Рисунок 12 Idempotency
Если обнаружены несколько concurrent requests с одинаковым idempotency key, обрабатывается только
один request, а остальные получают status code "429 Too Many Requests".
Чтобы поддержать idempotency, можно использовать unique key constraint database. Например, primary key database table и будет служить idempotency key. Это работает так:
-
Когда payment system получает payment, она пытается вставить row в database table.
-
Успешная вставка означает, что мы еще не видели этот payment request.
-
Если вставка падает, потому что такой primary key уже существует, значит этот payment request уже был. Второй request обрабатываться не будет.
Scenario 2: PSP успешно обработала payment, но response не дошел до нашей payment system из-за network errors. Затем user снова нажимает “pay”.
Как показано на рисунке 4 (шаги 2 и 3), payment service отправляет в PSP nonce, а PSP возвращает соответствующий token. Nonce уникально представляет payment order, а token уникально соответствует этому nonce. Следовательно, token однозначно соответствует payment order.
Когда user снова нажимает кнопку “pay”, payment order остается тем же, а значит token, отправляемый в PSP, тоже тот же. Так как token используется как idempotency key на стороне PSP, PSP способна распознать double payment и вернуть status предыдущего execution.
Consistency
Во время исполнения payment вызывается несколько stateful services:
-
Payment service хранит payment-related data: nonce, token, payment order, execution status и т. д.
-
Ledger хранит все accounting data.
-
Wallet хранит merchant account balance.
-
PSP хранит payment execution status.
-
Data может реплицироваться между разными database replicas для повышения reliability.
В distributed environment communication между любыми двумя services может завершиться failure, что приводит к data inconsistency. Посмотрим на несколько techniques, которые помогают решать inconsistency в payment system.
Чтобы поддерживать data consistency между internal services, очень важно обеспечить exactly-once processing.
Чтобы поддерживать data consistency между internal service и external service (PSP), мы обычно полагаемся на idempotency и reconciliation. Если external service поддерживает idempotency, нужно использовать тот же idempotency key для payment retry operations. Но даже если external service поддерживает idempotent API, reconciliation все равно нужна: нельзя предполагать, что external system всегда права.
Если data реплицируется, replication lag может привести к inconsistent data между primary database и replicas. Обычно есть два варианта решения:
-
Обслуживать и reads, и writes только через primary database. Такой подход легко настроить, но у него очевидный минус — scalability. Replicas используются только для data reliability, но не обслуживают traffic, а значит ресурсы расходуются неэффективно.
-
Обеспечить, чтобы все replicas всегда были in-sync. Для этого можно использовать consensus algorithms, например Paxos [21] и Raft [22], либо consensus-based distributed databases, например YugabyteDB [23] или CockroachDB [24].
Payment security
Payment security чрезвычайно важна. В заключительной части этого system design коротко рассмотрим несколько techniques, которые помогают бороться с cyberattacks и card theft.
| Problem | Solution |
|---|---|
| Request/response eavesdropping | Использовать HTTPS |
| Data tampering | Обязательное encryption и integrity monitoring |
| Man-in-the-middle attack | Использовать SSL с certificate pinning |
| Data loss | Database replication across multiple regions и snapshots данных |
| Distributed denial-of-service attack (DDoS) | Rate limiting и firewall [25] |
| Card theft | Tokenization. Вместо реальных card numbers хранятся и используются tokens |
| PCI compliance | PCI DSS — стандарт information security для организаций, работающих с branded credit cards |
| Fraud | Address verification, card verification value (CVV), user behavior analysis и т. д. [26] [27] |
Таблица 6 Payment security
Шаг 4 — Итоги
В этой главе мы разобрали pay-in flow и pay-out flow. Довольно подробно остановились на retry, idempotency и consistency. В конце главы также рассмотрели payment error handling и security.
Payment system чрезвычайно сложна. Несмотря на то что мы уже обсудили много тем, есть еще немало важных направлений, которые тоже стоит упомянуть. Ниже приведен representative, хотя и не exhaustive, список relevant topics.
-
Monitoring. Monitoring key metrics — критически важная часть любого modern application. Благодаря хорошему monitoring мы можем отвечать на вопросы вроде “Каков average acceptance rate для конкретного payment method?” или “Какова CPU usage наших servers?” Эти metrics можно собирать и отображать на dashboard.
-
Alerting. Когда происходит что-то abnormal, важно своевременно уведомить on-call developers, чтобы они быстро отреагировали.
-
Debugging tools. “Почему payment fail-ится?” — очень частый вопрос. Чтобы debugging был проще и для engineers, и для customer support, важно иметь tools, позволяющие просматривать transaction status, processing server history, PSP records и т. д. для конкретной payment transaction.
-
Currency exchange. Currency exchange — важная тема при проектировании payment system для international user base.
-
Geography. В разных regions наборы payment methods могут радикально отличаться.
-
Cash payment. Наличные платежи очень распространены в India, Brazil и некоторых других странах. Uber [28] и Airbnb [29] публиковали подробные engineering blogs о том, как они работали с cash-based payment.
-
Google/Apple Pay integration. Если интересно, посмотрите [30].
Поздравляю, если дочитали до этого места. Можно похлопать себе по плечу. Отличная работа!
Chapter Summary

Reference Materials
[1] Payment system: https://en.wikipedia.org/wiki/Payment_system
[2] AML/CFT: https://en.wikipedia.org/wiki/Money_laundering
[3] Card scheme: https://en.wikipedia.org/wiki/Card_scheme
[4] ISO 4217: https://en.wikipedia.org/wiki/ISO_4217
[5] Stripe API Reference: https://stripe.com/docs/api
[6] Double-entry bookkeeping: https://en.wikipedia.org/wiki/Double-entry_bookkeeping
[7] Books, an immutable double-entry accounting database service:
https://developer.squareup.com/blog/books-an-immutable-double-entry-accounting-database-service/
[8] Payment Card Industry Data Security Standard:
https://en.wikipedia.org/wiki/Payment_Card_Industry_Data_Security_Standard
[9] Tipalti: https://tipalti.com/
[10] Nonce: https://en.wikipedia.org/wiki/Cryptographic_nonce
[11] Webhooks: https://stripe.com/docs/webhooks
[12] Customize your success page: https://stripe.com/docs/payments/checkout/custom-success-page
[13] 3D Secure: https://en.wikipedia.org/wiki/3-D_Secure
[14] Kafka Connect Deep Dive – Error Handling and Dead Letter Queues:
https://www.confluent.io/blog/kafka-connect-deep-dive-error-handling-dead-letter-queues/
[15] Reliable Processing in a Streaming Payment System:
https://www.youtube.com/watch?v=5TD8m7w1xE0&list=PLLEUtp5eGr7Dz3fWGUpiSiG3d_WgJe-KJ
[16] Chain Services with Exactly-Once Guarantees:
https://www.confluent.io/blog/chain-services-exactly-guarantees/
[17] Exponential backoff: https://en.wikipedia.org/wiki/Exponential_backoff
[18] Idempotence: https://en.wikipedia.org/wiki/Idempotence
[19] Stripe idempotent requests: https://stripe.com/docs/api/idempotent_requests
[20] Idempotency: https://developer.paypal.com/docs/platforms/develop/idempotency/
[21] Paxos: https://en.wikipedia.org/wiki/Paxos_(computer_science)
[22] Raft: https://raft.github.io/
[23] YugabyteDB: https://www.yugabyte.com/
[24] Cockroachdb:https://www.cockroachlabs.com/
[25] What is DDoS attack: https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/
[26] How Payment Gateways Can Detect and Prevent Online Fraud:
https://www.chargebee.com/blog/optimize-online-billing-stop-online-fraud/
[27] Advanced Technologies for Detecting and Preventing Fraud at Uber:
https://www.uber.com/en-PK/blog/project-radar-intelligent-early-fraud-detection/
[28] Re-Architecting Cash and Digital Wallet Payments for India with Uber Engineering:
https://eng.uber.com/india-payments/
[29] Scaling Airbnb’s Payment Platform:
https://medium.com/airbnb-engineering/scaling-airbnbs-payment-platform-43ebfc99b324
[30] Payments Integration at Uber: A Case Study:
https://www.youtube.com/watch?v=yooCE5B0SRA