Перейти к основному содержимому

Платежная система

В этой главе мы спроектируем 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.

Изображение показывает упрощенную модель online transaction на e-commerce website: buyer платит через credit card, деньги поступают на промежуточный bank account платформы, а затем переводятся seller-у.

Рисунок 1 Упрощенные pay-in и pay-out flows

Pay-in flow​

High-level design pay-in flow показан на рисунке 2. Разберем каждый component этой system.

Изображение показывает архитектуру pay-in flow: payment event поступает в payment service, затем создаются payment orders, которые через payment executor обрабатываются через внешние PSP; после этого обновляются wallet и ledger.

Рисунок 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 работает так:

  1. Когда user нажимает кнопку “place order”, генерируется payment event и отправляется в payment service.

  2. Payment service сохраняет payment event в database.

  3. Иногда один payment event может содержать несколько payment orders. Например, можно выбрать products от нескольких sellers в одном checkout process. Если e-commerce website разбивает такой checkout на несколько payment orders, payment service вызывает payment executor для каждого payment order.

  4. Payment executor сохраняет payment order в database.

  5. Payment executor вызывает внешний PSP, чтобы обработать credit card payment.

  6. После того как payment executor успешно обработал payment, payment service обновляет wallet, чтобы зафиксировать, сколько денег теперь у конкретного seller-а.

  7. Wallet server сохраняет обновленную balance information в database.

  8. После того как wallet service успешно обновила balance information seller-а, payment service вызывает ledger, чтобы обновить его.

  9. 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 перечислены ниже:

FieldDescriptionType
buyer_infoИнформация о buyer-еjson
checkout_idГлобально уникальный ID этого checkout-аstring
credit_card_infoЭто может быть encrypted credit card information или payment token. Значение зависит от PSP.json
payment_ordersСписок payment orderslist

Таблица 1 Параметры API request-а (исполнение payment event)

payment_orders выглядят так:

FieldDescriptionType
seller_accountКакой seller получит деньгиstring
amountTransaction amount для order-аstring
currencyCurrency для 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 здесь — плохой выбор по двум причинам:

  1. Разные protocols, software и hardware могут поддерживать разную numeric precision при serialization и deserialization. Из-за этого легко получить нежелательные rounding errors.

  2. Число может быть очень большим (например, GDP Японии за 2020 календарный год — около 5x10^14 yen), а может быть очень маленьким (например, один 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 обычно не самый важный фактор. Вместо этого мы смотрим на следующее:

  1. Проверенная стабильность. Использовалась ли storage system большими financial firms на протяжении многих лет (например, более 5 лет) с положительным feedback.

  2. Богатство supporting tools, например monitoring и investigation tools.

  3. Зрелость рынка DBA. Возможность нанять experienced DBAs — очень важный фактор.

Обычно мы предпочитаем traditional relational database с ACID transaction support, а не NoSQL/NewSQL.

Таблица payment_event содержит детальную информацию о payment event. Она выглядит так:

NameType
checkout_idstring PK
buyer_infostring
seller_infostring
credit_card_infoзависит от card provider
is_payment_doneboolean

Таблица 3 Payment event

Таблица payment_order хранит execution status каждого payment order. Она выглядит так:

NameType
payment_order_idString PK
buyer_accountstring
amountstring
currencystring
checkout_idstring FK
payment_order_statusstring
ledger_updatedboolean
wallet_updatedboolean

Таблица 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. Логика обновления такая:

  1. Начальный статус payment_order_status — NOT_STARTED.

  2. Когда payment service отправляет payment order в payment executor, payment_order_status становится EXECUTING.

  3. 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).

AccountDebitCredit
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.

Изображение показывает hosted payment page PayPal, где user вводит email, password или выбирает оплату debit/credit card; сбор sensitive payment data выполняет сам PSP.

Рисунок 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 одним из двух способов:

  1. Если компания умеет безопасно хранить sensitive payment information и сознательно выбирает этот путь, PSP можно интегрировать через API. Компания сама отвечает за разработку payment web pages, сбор и хранение sensitive payment information. PSP отвечает за подключение к banks или card schemes.

  2. Если компания решает не хранить sensitive payment information из-за сложных regulations и security concerns, PSP предоставляет hosted payment page для сбора card payment details и безопасного хранения их у себя. Именно этот подход выбирает большинство компаний.

Используем рисунок 4, чтобы подробно объяснить, как работает hosted payment page.

Изображение показывает hosted payment flow: checkout page вызывает payment service, та регистрирует payment в PSP через nonce, получает token, сохраняет его, затем client открывает hosted payment page PSP, после чего результат приходит через redirect URL и webhook.

Рисунок 4 Hosted payment flow

Для простоты на рисунке 4 мы опустили payment executor, ledger и wallet. Payment service оркестрирует весь payment process.

  1. User нажимает кнопку “checkout” в client browser. Client вызывает payment service, передавая payment order information.

  2. Получив 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.

  3. PSP возвращает token обратно в payment service. Token — это UUID на стороне PSP, который уникально идентифицирует payment registration. Позже по этому token можно посмотреть payment registration и execution status.

  4. Payment service сохраняет token в database до того, как вызвать PSP-hosted payment page.

  5. После того как 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.

Изображение показывает hosted payment page Stripe с order summary слева и payment form справа; sensitive card data вводится на стороне PSP.

Рисунок 5 Hosted payment page by Stripe

  1. User заполняет payment details на web page PSP: credit card number, holder name, expiration date и т. д., затем нажимает кнопку pay. PSP начинает payment processing.

  2. PSP возвращает payment status.

  3. Web page перенаправляется на redirect URL. Payment status, полученный на шаге 7, обычно добавляется к URL. Например, полный redirect URL может выглядеть так [12]: https://your-company.com/?tokenID=JIOUIQ123NSF&payResult=X324FSa

  4. Асинхронно 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.

Изображение показывает место reconciliation в payment system: данные из ledger, wallet и отчетов PSP сравниваются для поиска расхождений.

Рисунок 6 Reconciliation

Reconciliation также используется для проверки внутренней consistency payment system. Например, состояния в ledger и wallet могут разойтись, и reconciliation system позволит обнаружить это расхождение.

Чтобы исправлять mismatches, найденные при reconciliation, обычно привлекают finance team для manual adjustments. Обычно mismatches и adjustments делят на три категории:

  1. Mismatch можно классифицировать, и adjustment можно автоматизировать. В этом случае мы знаем причину mismatch, знаем, как ее исправить, и написание automation экономически оправдано. Engineers могут автоматизировать и classification mismatch, и adjustment.

  2. Mismatch можно классифицировать, но автоматизировать adjustment нельзя. В этом случае мы понимаем причину mismatch и способ исправления, но стоимость написания auto-adjustment program слишком высока. Такой mismatch помещается в job queue, и finance team исправляет его вручную.

  3. 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 и m2 consumed service A и service B соответственно, оба messages удаляются из queue, как показано на рисунке 8.

Изображение показывает message queue, из которой сообщения распределяются между Service A и Service B.

Рисунок 7 Message queue

Изображение показывает single-receiver pattern: каждое сообщение из queue достается только одному service.

Рисунок 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.

Изображение показывает multiple-receiver pattern: события публикуются в message queue и затем обрабатываются payment system, analytics и billing.

Рисунок 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, чтобы можно было изучить, почему они не были успешно обработаны.

Изображение показывает flow обработки failed payments: retryable errors идут в retry queue, а после превышения лимита попыток — в dead letter queue.

Рисунок 10 Handle failed payments

  1. Проверяем, является ли failure retryable.

    1a. Retryable failures направляются в retry queue.

    1b. Non-retryable failures, например invalid input, сохраняются в database.

  2. Payment system читает events из retry queue и повторяет failed payment transactions.

  3. Если 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, если:

  1. Она выполняется at-least-once.

  2. И одновременно выполняется 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 прошел с четвертой попытки.

Изображение показывает sequence diagram: client несколько раз повторяет payment request на $10, пока payment system не отвечает успешным result.

Рисунок 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-а.

Изображение показывает idempotency в payment system: первый POST request с idempotency key обрабатывается, а повторный request с тем же key возвращает прежний result без повторного списания.

Рисунок 12 Idempotency

Если обнаружены несколько concurrent requests с одинаковым idempotency key, обрабатывается только один request, а остальные получают status code "429 Too Many Requests".

Чтобы поддержать idempotency, можно использовать unique key constraint database. Например, primary key database table и будет служить idempotency key. Это работает так:

  1. Когда payment system получает payment, она пытается вставить row в database table.

  2. Успешная вставка означает, что мы еще не видели этот payment request.

  3. Если вставка падает, потому что такой 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:

  1. Payment service хранит payment-related data: nonce, token, payment order, execution status и т. д.

  2. Ledger хранит все accounting data.

  3. Wallet хранит merchant account balance.

  4. PSP хранит payment execution status.

  5. 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. Обычно есть два варианта решения:

  1. Обслуживать и reads, и writes только через primary database. Такой подход легко настроить, но у него очевидный минус — scalability. Replicas используются только для data reliability, но не обслуживают traffic, а значит ресурсы расходуются неэффективно.

  2. Обеспечить, чтобы все 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.

ProblemSolution
Request/response eavesdroppingИспользовать HTTPS
Data tamperingОбязательное encryption и integrity monitoring
Man-in-the-middle attackИспользовать SSL с certificate pinning
Data lossDatabase replication across multiple regions и snapshots данных
Distributed denial-of-service attack (DDoS)Rate limiting и firewall [25]
Card theftTokenization. Вместо реальных card numbers хранятся и используются tokens
PCI compliancePCI DSS — стандарт information security для организаций, работающих с branded credit cards
FraudAddress 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​

Изображение показывает mind map главы о Payment Service: requirements, pay-in/pay-out flow, reconciliation, handling delays, failed payments, exactly-once delivery, consistency и payment security.

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