Новаком
ФИНТЕХ

Архитектура платёжных систем: процессинг, эквайринг и надёжность под нагрузкой

Как устроен платёж end-to-end — от мерчанта до банка-эмитента, идемпотентность и согласованность транзакций, интеграция платёжных шлюзов, реконсиляция, PCI DSS и токенизация, highload-паттерны (outbox, очереди, ретраи) и импортозамещение платёжной инфраструктуры на Java/Kotlin.

ЯА
Яковлев Александр
2026-06-28 · 17 минут чтения

Содержание

Платёжная система выглядит просто ровно до того момента, пока вы не начинаете её строить. Снаружи это «пользователь выбирает тариф и оплачивает через эквайринг» — а внутри десятки состояний, гарантии согласованности, реконсиляция с банком, требования регулятора и инфраструктура, которая не имеет права упасть в пятницу вечером. В наших корпоративных каналах постоянно всплывает один и тот же сюжет: задача, которая «на 20 минут», на деле оборачивается шестью-семью часами на один запрос — и это только интеграция, без учёта эксплуатации.

Эта статья — практический разбор того, как устроены платёжные и финтех-системы с точки зрения инженера: как проходит платёж от мерчанта до банка-эмитента, какие требования критичны (идемпотентность, согласованность, аудит, отказоустойчивость, SLA), как интегрировать платёжные шлюзы, обрабатывать ошибки и сводить деньги в реконсиляции, и как всё это держать под нагрузкой. Примеры — на Java/Kotlin и Spring, потому что именно на этом стеке мы в Новакоме чаще всего и проектируем финтех-контур.

Почему «прикрутить оплату» — это не 20 минут работы

Начнём с трезвого взгляда на сложность, потому что недооценка здесь стоит дороже всего.

Типичный запрос из реальной практики звучит так: «нам нужна интеграция платёжного сервиса с облачной кассой — чтобы после покупки данные автоматически уходили в кассу, формировался чек и отправлялся покупателю; сейчас делаем вручную, это неудобно». В общих чертах задача кажется тривиальной: вебхук, пара HTTP-запросов, готово. Но как только начинаешь углубляться, всплывает фискализация по 54-ФЗ, форматы чеков, повторная отправка при сбое ОФД, дедупликация вебхуков, сверка сумм — и «20 минут» превращаются в полноценный проект.

Второй слой сложности — юридический и продуктовый. B2C-сценарий действительно прост: оферта, эквайринг, оплата тарифа. Но стоит выйти в B2B — и нужны договор как основание платежа, закрывающие документы (УПД для бухгалтерии), сверка взаиморасчётов. Платёжная логика перестаёт быть «кнопкой оплатить» и становится частью документооборота.

Третий слой — экономика и выбор провайдера. Комиссии за онлайн-эквайринг гуляют в районе 0,8–2,5% и сильно зависят от банка и оборота; правильный выбор эквайера может ощутимо снизить расходы. На рынке десятки шлюзов с разным покрытием карточных систем, разной поддержкой фискализации и разными SLA. Кто-то берёт фискализацию на себя и шлёт онлайн-чеки сам, кто-то требует связки с ОФД и онлайн-кассой. Это архитектурное решение, а не строчка в конфиге.

Вывод простой: платёжная система — это не интеграция, а распределённая система с деньгами внутри, где цена ошибки измеряется не в багрепортах, а в рублях и в претензиях регулятора.

Как устроен платёж end-to-end

Чтобы проектировать платёжный контур, нужно держать в голове полную цепочку участников. Карточный платёж проходит примерно так:

  1. Мерчант (ваш бэкенд). Инициирует платёж: создаёт заказ, формирует сумму, дёргает платёжный шлюз. Своих данных карты не хранит и в идеале вообще не видит.
  2. Платёжный шлюз (payment gateway). Принимает запрос, собирает данные карты на своей стороне (через платёжную форму или токен), маршрутизирует транзакцию к эквайеру. Это та «прослойка», которую обычно и интегрирует разработчик: ЮKassa, CloudPayments, Робокасса, банковский эквайринг напрямую.
  3. Банк-эквайер (acquirer). Банк, обслуживающий мерчанта. Отправляет авторизационный запрос в платёжную систему.
  4. Платёжная система (card scheme). МИР, Visa, Mastercard — маршрутизирует запрос к банку, выпустившему карту. По сути это «паритет возможности» ускоренных и упрощённых расчётов между банками, выстроенный десятилетиями.
  5. Банк-эмитент (issuer). Банк держателя карты. Проверяет баланс, лимиты, антифрод, 3-D Secure и отвечает: одобрить или отклонить.

Ответ возвращается по той же цепочке обратно. Важно понимать ключевую развилку платёжной механики — двухстадийность:

  • Авторизация (authorization / hold). Эмитент замораживает сумму на карте, но деньги ещё не списаны. Мерчант получает подтверждение, что средства есть.
  • Списание (capture / clearing). Подтверждение реального перевода. Может произойти сразу (одностадийная схема) или позже — например, после отгрузки товара (двухстадийная).
  • Отмена/возврат (void / refund). Снятие холда до клиринга или возврат уже списанных средств.

Отдельно живёт расчётный цикл (settlement): фактические деньги между банками двигаются не в момент авторизации, а пакетами, по итогам дня. Именно поэтому в финтех-системах сеттлмент-данные — отдельная сущность, и сверка дашбордов по 20 миллионам транзакций в месяц — это самостоятельная аналитическая задача, а не побочный эффект процессинга.

Для инженера из этой картины следует главное: между «пользователь нажал оплатить» и «деньги на счёте мерчанта» лежит распределённая транзакция через 4–5 независимых систем, каждая из которых может ответить с задержкой, ответить ошибкой или не ответить вовсе. Вся архитектура платёжного сервиса строится вокруг этого факта.

Идемпотентность: главное требование процессинга

Самая частая и самая дорогая ошибка в платёжных системах — двойное списание. Возникает оно банально: клиент нажал «оплатить» дважды, мобильное приложение повторило запрос по таймауту, балансировщик отправил ретрай, вебхук от шлюза пришёл трижды. В обычном CRUD это раздражает; в платежах это потерянные деньги и разбирательство с клиентом.

Решение — идемпотентность: повторный запрос с тем же ключом должен приводить к тому же результату, не создавая новый платёж. Практически это реализуется через идемпотентный ключ (idempotency key), который клиент генерирует один раз на одну попытку оплаты и передаёт во всех ретраях.

@Service
class PaymentService(
    private val payments: PaymentRepository,
    private val gateway: PaymentGatewayClient,
) {
    /**
     * Возвращает существующий платёж по ключу идемпотентности
     * либо атомарно создаёт новый. Повторный вызов с тем же ключом
     * никогда не приводит к двойному списанию.
     */
    @Transactional
    fun authorize(command: AuthorizeCommand): Payment {
        // Быстрый путь: платёж по этому ключу уже есть
        payments.findByIdempotencyKey(command.idempotencyKey)?.let { existing ->
            require(existing.amount == command.amount) {
                "Idempotency key reuse with different amount"
            }
            return existing
        }

        // Резервируем запись со статусом PENDING.
        // Уникальный индекс на idempotency_key — последняя линия обороны
        // от гонки двух параллельных запросов.
        val payment = try {
            payments.save(Payment.pending(command))
        } catch (e: DataIntegrityViolationException) {
            return payments.findByIdempotencyKey(command.idempotencyKey)
                ?: throw e
        }

        val result = gateway.authorize(payment.toGatewayRequest())
        payment.applyAuthorization(result)
        return payments.save(payment)
    }
}

Ключевые моменты, которые часто упускают:

  • Уникальный индекс в БД на idempotency_key — не «оптимизация», а обязательное условие корректности. Проверка «есть ли уже платёж» в коде не спасает от гонки двух одновременных запросов; спасает констрейнт базы.
  • Ключ привязан к параметрам платежа. Если по тому же ключу пришла другая сумма — это ошибка клиента, а не повтор. Такой запрос нужно отклонять.
  • Идемпотентность сквозная. Её надо протягивать и в вызов шлюза (большинство серьёзных провайдеров принимают Idempotency-Key в заголовке), и в обработку вебхуков, и в фискализацию.

Состояние транзакции и согласованность

Платёж — это конечный автомат (state machine). Описать его явно и запретить недопустимые переходы — половина успеха. Размазанные по коду if (status == ...) рано или поздно приведут к платежу, который «завис» — а зависший платёж это всегда либо недополученные, либо дважды списанные деньги.

enum class PaymentStatus { PENDING, AUTHORIZED, CAPTURED, FAILED, REFUNDED, CANCELLED }

private val allowedTransitions: Map<PaymentStatus, Set<PaymentStatus>> = mapOf(
    PaymentStatus.PENDING    to setOf(PaymentStatus.AUTHORIZED, PaymentStatus.FAILED, PaymentStatus.CANCELLED),
    PaymentStatus.AUTHORIZED to setOf(PaymentStatus.CAPTURED, PaymentStatus.CANCELLED),
    PaymentStatus.CAPTURED    to setOf(PaymentStatus.REFUNDED),
    PaymentStatus.FAILED      to emptySet(),
    PaymentStatus.REFUNDED    to emptySet(),
    PaymentStatus.CANCELLED   to emptySet(),
)

fun Payment.transitionTo(target: PaymentStatus) {
    check(target in allowedTransitions.getValue(status)) {
        "Illegal transition $status -> $target for payment $id"
    }
    status = target
    updatedAt = Instant.now()
}

Второй важный аспект — согласованность под конкурентным доступом. Когда вебхук об успешной авторизации и фоновый процесс отмены по таймауту приходят одновременно, без блокировки они затрут друг друга. Здесь работает оптимистичная блокировка через версионирование (@Version в JPA): транзакция, чья версия устарела, откатывается и повторяется на свежих данных.

@Entity
class Payment(
    @Id val id: UUID,
    @Version var version: Long = 0,
    @Enumerated(EnumType.STRING) var status: PaymentStatus = PaymentStatus.PENDING,
    val amount: Money,
    val idempotencyKey: String,
)

И третье — аудит. В платёжной системе каждое изменение состояния должно быть записано неизменяемой строкой в журнал: кто, когда, с какого состояния на какое, по какому событию. Это требование не только ИБ, но и реконсиляции: когда деньги «не сходятся», восстанавливать историю приходится именно по аудит-логу. Здесь не бывает «100% надёжных систем без сбоев» — поэтому возможность достоверно восстановить, что произошло, важнее, чем вера в то, что сбоев не будет.

Интеграция платёжных шлюзов

На практике почти никто не подключается напрямую к платёжной системе — это лицензии, сертификация и полмиллиона евро с годом ожидания первой транзакции. Подключаются к шлюзу. И здесь главный архитектурный приём — не зашивать конкретного провайдера в бизнес-логику, а спрятать его за абстракцией. Сегодня у вас один эквайер, завтра вы добавляете второй для маршрутизации по картам (классическая боль: один шлюз работает только с Mastercard, для Visa нужен другой), послезавтра меняете провайдера ради комиссии на 0,3% ниже.

interface PaymentGateway {
    fun authorize(request: AuthorizeRequest): GatewayResult
    fun capture(transactionId: String, amount: Money): GatewayResult
    fun refund(transactionId: String, amount: Money): GatewayResult
    fun parseWebhook(payload: ByteArray, signature: String): WebhookEvent
}

@Component
class GatewayRouter(private val gateways: Map<CardScheme, PaymentGateway>) {
    fun forCard(scheme: CardScheme): PaymentGateway =
        gateways[scheme] ?: error("No gateway configured for $scheme")
}

Несколько правил, которые экономят недели на интеграции:

  • Вебхуки — основной источник истины о финальном статусе, а не ответ на синхронный запрос. Сеть может оборвать соединение после того, как платёж прошёл; статус вы узнаете из вебхука. Поэтому синхронный ответ обрабатываем оптимистично, но финал фиксируем по асинхронному уведомлению.
  • Каждый вебхук проверяем по подписи и дедуплицируем. Провайдеры доставляют уведомления «хотя бы один раз» — дубли неизбежны. Спасает тот же идемпотентный подход: уникальный ключ события и проверка «не обработали ли уже».
  • Не доверяем суммам из вебхука вслепую — сверяем с тем, что инициировали. Это и защита от ошибок, и часть антифрода.
  • Таймауты и параметры ретраев настраиваются на каждый шлюз отдельно. У провайдеров разный SLA, и одинаковый таймаут на всех — путь к ложным «зависаниям».

Подробнее об организации биллинговой логики поверх шлюзов мы писали в разборе архитектуры SaaS-биллинга на Spring Boot — там акцент на тарифах, подписках и закрывающих документах, которые в B2B неотделимы от платежа.

Обработка ошибок и реконсиляция

В платежах ошибки делятся на два класса, и путать их нельзя.

Детерминированные отказы — недостаточно средств, неверный CVV, карта заблокирована, лимит эмитента. Их ретраить бессмысленно: ответ не изменится. Такой платёж переводим в FAILED и честно сообщаем пользователю причину.

Недетерминированные сбои — таймаут, обрыв сети, 503 от шлюза, отсутствие ответа. Здесь самое опасное состояние во всём процессинге: вы не знаете, прошёл платёж или нет. Деньги могли списаться, а ответ — потеряться. Нельзя ни считать платёж успешным, ни просто повторить его (рискуя двойным списанием). Правильная стратегия — повторный запрос с тем же идемпотентным ключом либо явный запрос статуса у провайдера (status reconciliation), пока не получим определённый ответ.

Отсюда вырастает реконсиляция — регулярная сверка вашего состояния платежей с выпиской провайдера и банка. Это не опция, а обязательный процесс: банки обновляют данные в своих системах с задержкой, и расхождения накапливаются. Типовая реконсиляция выглядит так:

Сценарий расхожденияЧто значитДействие
У нас CAPTURED, у банка нетСписание не дошло или потеряноРасследование, запрос статуса, при необходимости — повторный capture
У банка списание, у нас PENDING/FAILEDПотерян вебхук или сбой на нашей сторонеДослать в учёт, при невозможности — возврат
Суммы не совпадаютЧастичный возврат, комиссия, ошибкаРучной разбор по аудит-логу
У нас REFUNDED, у банка нет возвратаВозврат не инициировался реальноПовторить refund

Практический подход — ночной батч, который выгружает реестр операций провайдера, сопоставляет с базой по внешнему ID транзакции и формирует отчёт о расхождениях. То, что свелось автоматически, закрывается; остальное уходит на ручной разбор операторам. Без такого процесса деньги «теряются» тихо, и обнаруживается это уже на уровне бухгалтерии в конце месяца.

Безопасность: PCI DSS, токенизация, 152-ФЗ

Деньги притягивают атаки, поэтому безопасность в платёжной системе — это не «слой поверх», а часть архитектуры с первого дня.

PCI DSS и принцип «не хранить то, что не обязан». Самое дорогое требование стандарта — защита данных карты (PAN, CVV). Лучший способ его выполнить — вообще не пропускать данные карты через свой бэкенд. Здесь работает токенизация: платёжная форма шлюза собирает карту на своей стороне и возвращает вам токен — безопасный суррогат, которым можно проводить повторные списания, но из которого нельзя восстановить номер карты. Так вы радикально сокращаете область PCI DSS-аудита: ваш сервер видит токены, а не карты.

Токены вместо карт для рекуррентных платежей. Подписки, автосписания, сохранённые карты — всё это строится на токенах, а не на хранении PAN. Токен привязан к мерчанту и провайдеру, и его утечка не даёт злоумышленнику возможности расплатиться картой в другом месте.

3-D Secure и антифрод. Дополнительная аутентификация держателя (редирект на страницу банка, push, OTP) переносит ответственность за мошеннические транзакции на эмитента и режет фрод. Антифрод-правила (лимиты на частоту, гео, суммы) — обязательный контур: операции вроде регулярного снятия крупных сумм или нетипичной географии должны как минимум флагироваться.

152-ФЗ и контур данных. Для российского рынка персональные и платёжные данные граждан РФ должны обрабатываться и храниться в инфраструктуре на территории России. Это влияет на выбор хостинга, провайдеров и архитектуру: критичные данные не должны утекать во внешние SaaS за периметром. Self-hosted-компоненты и хранение событий под собственным контролем здесь не идеология, а требование закона.

Отдельно стоит помнить про принцип минимизации доступа и сегментацию: контур, где ходят платёжные данные, изолируется от остального, доступ — по ролям (RBAC), а каждое обращение к чувствительным данным логируется. Истории вроде многократных взломов хранилищ паролей напоминают простую вещь: один инцидент — случайность, система инцидентов — приговор сервису. В платежах второго шанса обычно не дают.

Highload: очереди, outbox, ретраи

Платёжная система обязана быть быстрой и не падать под пиком — будь то распродажа, зарплатный день или массовая рассылка счетов. Архитектура отказоустойчивости здесь стоит на нескольких опорах.

Балансировка и горизонтальное масштабирование. Stateless-сервисы процессинга за балансировщиком, реплики для отказоустойчивости, шлюз авторизации как отдельный компонент со своей БД, контроль лимитов как отдельный сервис — типовая схема, которую подтверждает практика highload-платёжных контуров. Если один сервис «затормозит», он не должен ронять остальные — поэтому критичны изоляция и контроль лимитов.

Transactional outbox — решение фундаментальной проблемы: как атомарно и записать платёж в БД, и отправить событие в Kafka? Двухфазный коммит дорог и хрупок; вместо него событие пишется в таблицу outbox в той же транзакции, что и изменение платежа, а отдельный процесс публикует его в брокер с гарантией at-least-once.

@Transactional
fun capturePayment(paymentId: UUID): Payment {
    val payment = payments.findByIdOrNull(paymentId) ?: error("not found")
    payment.transitionTo(PaymentStatus.CAPTURED)

    // Платёж и событие пишутся в одной транзакции —
    // либо оба, либо ничего. Публикацию в Kafka берёт на себя
    // отдельный релейер, читающий таблицу outbox.
    outbox.save(
        OutboxEvent(
            aggregateId = payment.id,
            type = "PaymentCaptured",
            payload = payment.toEventJson(),
        )
    )
    return payments.save(payment)
}

Детальный разбор этого паттерна с релейером и Kafka — в статье про outbox в Spring Boot на проде. Для платежей это базовый кирпич: он гарантирует, что после успешного списания фискализация, уведомление и реконсиляция точно получат своё событие, даже если сервис упал сразу после коммита.

Очереди для развязки. Фискализация чека, отправка уведомлений, выгрузка в учётные системы — всё это асинхронно через очередь. Синхронный путь оплаты должен быть максимально коротким; всё, что можно сделать после подтверждения, делается после. Это и про скорость, и про устойчивость: если ОФД временно недоступен, чеки копятся в очереди и досылаются, а не блокируют платежи.

Circuit breaker и грамотные ретраи. Когда внешний шлюз деградирует, нельзя долбить его ретраями — это усугубляет аварию и копит таймауты. Размыкатель цепи (circuit breaker) временно отсекает обращения к упавшему провайдеру, давая ему восстановиться, а ретраи делаются с экспоненциальной задержкой и джиттером — и только для недетерминированных ошибок. Как это собирается на практике, мы показывали в гайде по Resilience4j и circuit breaker в Spring Boot. Более широкий взгляд на проектирование нагруженных систем — в материале про highload-архитектуру на Java.

Импортозамещение платёжной инфраструктуры

Для российского финтеха к общим требованиям добавляется ещё один вектор — независимость от зарубежной инфраструктуры. И это не абстракция: проблемы приёма карт Visa за рубежом, сложности с выводом средств между юрисдикциями, необходимость работать только через МИР для части операций — всё это реальные ограничения, с которыми сталкиваются команды.

Что это значит для архитектуры:

  • Процессинг через МИР и российские платёжные системы как основной контур, с абстракцией над шлюзами, которая позволяет добавлять и переключать эквайеров без переписывания бизнес-логики.
  • Self-hosted-компоненты вместо внешних SaaS там, где речь о критичных данных. Зависеть в приёме платежей от сервиса, который может отключить доступ, — недопустимый единичный риск.
  • Отечественные средства безопасности — токены, HSM, криптография по российским стандартам там, где это требуется регулятором.
  • Данные внутри периметра — хранение событий, сессий и платёжных данных под собственным контролем, что одновременно закрывает и 152-ФЗ, и риск внешней точки отказа.

Здесь же стоит трезво оценивать сборку «своей платёжки». Построить полноценное платёжное учреждение с нуля — это лицензии, год до первой транзакции и серьёзный капитал; для большинства бизнесов правильный путь — не строить процессинг с нуля, а грамотно собрать систему поверх существующих эквайеров и шлюзов, заложив в неё корректную обработку денег, отказоустойчивость и возможность менять провайдеров. Импортозамещение здесь — это про архитектурную независимость и контроль над данными, а не про переписывание платёжных систем целиком.

FAQ

Чем финтех-сервис отличается от банка? Банк работает на основе банковской лицензии и сам является конечным звеном расчётов. Финтех-сервис, как правило, лицензии не имеет и работает поверх банковской инфраструктуры — он добавляет технологичность, удобство и автоматизацию (онбординг, API, аналитику, диспуты), но опирается на банки и платёжные системы в основе. Масштабировать платёжный инструмент без банковской инфраструктуры невозможно — поэтому серьёзный финтех всегда строится в связке с банками.

Зачем нужна идемпотентность, если есть транзакции в БД? Транзакция БД гарантирует атомарность внутри вашей базы, но не защищает от повторного вызова всей операции — например, когда клиент дважды нажал «оплатить» или шлюз прислал дубль вебхука. Идемпотентный ключ гарантирует, что повтор приведёт к тому же результату и не создаст второй платёж. Это разные уровни защиты, и нужны оба.

Можно ли хранить данные карт у себя, чтобы делать повторные списания? На практике — нет, не нужно. Для рекуррентных платежей используется токенизация: шлюз возвращает токен, которым можно проводить списания, не храня номер карты. Это радикально сокращает область PCI DSS-аудита и снимает с вас самые тяжёлые требования по защите карточных данных.

Что делать, если платёж «завис» — ответа от шлюза нет? Нельзя считать его ни успешным, ни неуспешным. Нужно повторить запрос с тем же идемпотентным ключом или явно запросить статус транзакции у провайдера, пока не придёт определённый ответ. Окончательную истину дают вебхук и ночная реконсиляция с выпиской провайдера.

Нужна ли реконсиляция, если шлюз присылает вебхуки? Да. Вебхуки теряются, приходят с задержкой, дублируются; банки обновляют данные в своих системах не мгновенно. Реконсиляция — независимая сверка вашего состояния с реестром провайдера и банка — единственный способ гарантированно поймать расхождения до того, как они всплывут в бухгалтерии.


Если вы проектируете платёжный или финтех-контур — процессинг, эквайринг, биллинг или интеграцию шлюзов — мы в Новакоме строим такие системы на Java/Kotlin: от модели транзакций и идемпотентности до highload-инфраструктуры, реконсиляции и требований 152-ФЗ и PCI DSS. Посмотрите наши услуги по разработке на Spring и заказной разработке ПО или напишите нам с описанием вашей задачи — оценим объём честно, без «это на 20 минут».

РАЗРАБОТКА

Нужна похожая задача?

Обсудим вашу задачу и предложим решение за 30 минут.

Обсудить проект
ЧИТАЙТЕ ДАЛЬШЕ

Похожие материалы.

МЕЖБАНКОВСКИЕ-РАСЧЕТЫ

Межбанковская расчётная система: ностро, лоро и ликвидность на зарубежных счетах

Как устроена межбанковская расчётная система: принцип работы корреспондентских счетов ностро/востро/лоро, клиринг и неттинг (CHIPS, RTGS, SWIFT), способы образования ликвидности на зарубежных счетах — префандинг, FX-свопы, межбанковские кредиты, репо, инкассация — и инженерный взгляд на разработку такой системы.

2026-06-28 · 18 мин
ESP32

ESP32 и mesh-сети: ESP-NOW, ESP-WIFI-MESH, BLE Mesh и Thread/Matter

Серьёзный технический разбор mesh-сетей на ESP32: чем mesh отличается от обычного Wi-Fi, протоколы ESP-NOW, ESP-WIFI-MESH/Mesh-Lite, BLE Mesh и Thread/Matter на ESP32-H2, роли узлов, self-healing, реальные грабли (RSSI, расстояние между узлами, документация) и как выбрать протокол под задачу.

2026-06-28 · 17 мин
NVIDIA-JETSON

NVIDIA Jetson для edge AI: исследование возможностей платформы в 2026

Практический разбор NVIDIA Jetson как платформы edge AI: линейка Orin Nano/NX/AGX, метрика TOPS, реальные бенчмарки (YOLO v8 и локальная Llama 3.2), сравнение с Raspberry Pi, edge против облака, сценарии для роботов, дронов и компьютерного зрения, и когда Jetson реально оправдан.

2026-06-28 · 14 мин