Транспорт бэкенда
System Design: API Platform как Stripe
Stripe обрабатывает $1 триллион платежей в год. Каждый день миллионы разработчиков вызывают stripe.charges.create(). Что стоит за этими тремя словами - и почему Stripe тратит больше инженерных усилий на developer experience, чем большинство компаний на core продукт?
- **Stripe Idempotency Keys** обрабатывают тысячи дублированных запросов ежедневно - без этого механизма двойные списания были бы нормой из-за network timeouts
- **Twilio** генерирует SDK для 9 языков из единой OpenAPI спецификации: изменение в API автоматически попадает во все SDK через CI/CD pipeline, без ручной работы
- **GitHub API versioning** - заморозили /v3 на 10 лет пока разрабатывали GraphQL /v4, поддерживая обе версии параллельно для 2M+ интеграций
Idempotency Keys
Idempotency key - клиентский UUID который гарантирует что повторный запрос даёт тот же результат. Stripe требует Idempotency-Key header для всех write операций. Если network timeout - клиент повторяет запрос с тем же ключом. Сервер lookup'ает ключ в Redis/Postgres, находит cached response - возвращает его без повторной обработки. Ключ хранится 24 часа.
Stripe обрабатывает >1M API запросов в секунду. Без idempotency keys - тысячи двойных списаний в день из-за network retries. Правило: idempotency key должен быть scoped к пользователю (не глобально), иначе attacker может угадать ключ другого пользователя. Также важен конкурентный кейс: два одновременных запроса с одинаковым ключом - distributed lock предотвращает двойную обработку.
Клиент отправил POST /charge с Idempotency-Key: abc123. Запрос таймаутнул на клиенте. Сервер реально списал деньги и сохранил в БД, но не успел ответить. Клиент повторяет запрос с тем же ключом. Что вернёт сервер?
Webhook Delivery
Webhook - это HTTP callback: когда происходит событие (payment.succeeded), платформа делает POST на URL клиента. Проблема: клиентский сервер может быть недоступен. Гарантированная доставка требует: персистентная очередь (Kafka/DB), exponential backoff retry, signature verification, event ordering. Stripe повторяет webhook 64 часа с нарастающими интервалами (1m, 5m, 30m, 2h, ...).
Fan-out: один платёж может триггерить десятки webhooks (разные endpoints у одного merchant). При payment.succeeded - webhook для accounting, fraud system, CRM, analytics. Webhook delivery rate на Stripe: 99.97% доставка в течение первой минуты, 99.999% в течение 64 часов. Клиентам рекомендуется отвечать 200 в течение 30s и обрабатывать payload асинхронно.
Клиентский webhook endpoint возвращает 200 OK за 45 секунд (медленная обработка). Stripe timeout 30s. Что произойдёт?
API Versioning
API versioning - как платформа эволюционирует без поломки клиентов. Stripe использует date-based versioning: `Stripe-Version: 2024-06-20`. Каждый аккаунт привязан к версии на момент регистрации. Breaking change (удаление поля, изменение формата) требует новой версии. Stripe поддерживает 10+ лет назад API версий одновременно - это дорого, но необходимо.
Stripe migration strategy: при breaking change - создать новую версию, старая работает. Клиент видит в dashboard 'Your API version is 2020-03-02, upgrade available'. Auto-migration: Stripe тестирует каждый request текущей версии против новой версии в shadow mode, показывает diff клиенту перед обновлением. Это позволяет клиенту знать что именно сломается.
API возвращает поле `amount` как integer (cents). Планируется изменить на string (для поддержки multi-currency с десятичными). Это breaking change?
Rate Limit Design
Rate limiting для API платформы работает на нескольких уровнях: per-API-key (100 req/s для free tier, 1000 req/s для enterprise), per-endpoint (POST /charges = 50/s, GET /charges = 500/s), per-IP для защиты от scraping. Stripe возвращает `Retry-After` header и `X-RateLimit-Remaining` для прозрачности. Rate limit state хранится в Redis с sliding window.
Cost-based rate limiting: не все запросы стоят одинаково. GET /customers = 1 unit, POST /charges = 10 units (БД транзакция + fraud check), list операции с большим limit = 5 units. Stripe использует именно cost-based: каждый endpoint имеет вес, и лимит считается в units/second а не requests/second. Это честнее для клиентов которые делают много лёгких запросов.
API возвращает 429 с `Retry-After: 1`. Клиент немедленно ретраит через 1 секунду точно. Почему это плохо при 10K одновременных клиентов?
SDK Generation
SDK generation - автоматическое создание клиентских библиотек из OpenAPI/Protobuf спецификации. Stripe поддерживает официальные SDK для 7 языков (Ruby, Python, Node, PHP, Go, Java, .NET) - каждый генерируется из единого source of truth (stripe-openapi репозиторий). Без SDK - клиент должен сам разбираться в auth, idempotency, retry logic, pagination. С SDK - `stripe.charges.create({amount: 1000})`.
Pagination design критична для SDK: cursor-based (Stripe `starting_after` = last object ID) vs offset-based. Cursor-based стабилен при изменении данных между запросами (новые объекты не сдвигают offset). Stripe Python SDK: `for charge in stripe.Charge.auto_paging_iter()` - SDK автоматически делает следующий запрос с `starting_after=last_id`, клиент видит один большой итерируемый объект.
API versioning - это просто /v1/ и /v2/ в URL, остальное не важно
Versioning - это полная стратегия: семантика breaking vs non-breaking changes, shadow testing новой версии против старой, migration guides, deprecation timeline (минимум 12 месяцев), SDK обновления. Stripe поддерживает 50+ versions одновременно
Компании интегрируют API годами. Bank может годами не обновлять Stripe SDK. Публичный API - это контракт, нарушение которого разрушает доверие и ломает production у клиентов
API использует offset pagination: `?offset=100&limit=10`. Между двумя запросами клиента добавился новый объект в начало списка. Что произойдёт?
Итоги
- **Idempotency keys** - первая линия защиты от дублирования: клиентский UUID + server-side cache позволяет безопасно ретраить любой write запрос даже после network timeout
- **Webhook delivery** требует той же надёжности что и обычные API: персистентная очередь, exponential backoff 64+ часа, HMAC signature verification, idempotent обработчики на стороне клиента
- **Breaking change = новая версия** - строгое правило. Non-breaking (добавление полей) версии не требует. Поддержка старых версий годами - цена публичного API
Связанные темы
API Platform объединяет паттерны из нескольких областей курса:
- API Gateway — Rate limiting из API Platform реализуется в API Gateway слое: Kong/Envoy применяют per-key лимиты до того как запрос достигает бизнес-логики
- Dead Letter Queue — Webhook delivery engine - это DLQ паттерн: failed deliveries уходят в retry queue с exponential backoff, те же алгоритмы из урока про DLQ
- Transactional Outbox — Webhook events генерируются через Outbox Pattern: payment.succeeded записывается в БД атомарно с основной транзакцией, worker читает outbox и доставляет webhook
Вопросы для размышления
- Stripe поддерживает 50+ API версий одновременно. Какова реальная стоимость этого: сколько engineer-hours в год тратится на поддержку legacy версий vs выгода от backward compatibility?
- Idempotency key scoped к пользователю (userId + key). Если ключ - просто counter (1, 2, 3, ...), можно ли это атаковать? Как сделать ключи непредсказуемыми без потери удобства?
- SDK сгенерирован из OpenAPI. В production обнаружена ошибка в бизнес-логике (неверный расчёт). Нужно выпустить hotfix. Какие версии SDK нужно обновить: только последнюю или все поддерживаемые?