Перейти до вмісту

System design

Раунд system design перевіряє не знання конкретної технології, а вміння перетворити розмиту задачу на схему з цифрами й чесно назвати її слабкі місця. Тут — порядок розв’язання, прийоми масштабування, вибір рівня узгодженості, місце API Gateway і розбір типової задачі про масові сповіщення.

Перед цією темою: Черги й Kafka, Архітектурні стилі.

Junior+

Як підходити до задачі system design на співбесіді?

Section titled “Як підходити до задачі system design на співбесіді?”
Відповідь

Що це

System design — задача спроєктувати систему цілком: компоненти, дані та зв’язки між ними. Формулювання навмисно розмите: «спроєктуйте чат» або «сервіс коротких посилань».

Як користуватися

Порядок для інтерв’ю на 45 хвилин:

  1. Вимоги (5–10 хв). Що система робить і для скількох користувачів.
  2. Оцінка навантаження (3–5 хв). Скільки запитів на секунду й даних на рік.
  3. API та модель даних (5 хв). Основні ендпоінти й таблиці.
  4. Схема (10–15 хв). Клієнт, балансувальник, сервіси, база, кеш, черга.
  5. Вузькі місця (10–15 хв). Що зламається під навантаженням і як це виправити.
  6. Підсумки (3–5 хв). Короткий переказ рішення та його слабких місць.

Типова помилка

Мовчки малювати схему. Інтерв’юер оцінює міркування, тому кожне рішення проговорюють уголос.

Як влаштовано

  1. Функціональні вимоги. 3–5 головних сценаріїв: «створити коротке посилання», «перейти за ним». Решту явно виносять за межі.
  2. Нефункціональні вимоги. Кількість користувачів, співвідношення читання й запису, допустима затримка (p99), доступність, узгодженість, термін зберігання.
  3. Оцінка навантаження. RPS у середньому та в піку, обсяг даних за рік, вихідний трафік. Точність — до порядку.
  4. API. 2–4 ендпоінти з параметрами й відповіддю: POST /links, GET /{code}.
  5. Модель даних. Сутності, ключі та шаблони доступу — за ними обирають сховище.
  6. Високорівнева схема. Шлях запиту від клієнта до бази й назад.
  7. Вузькі місця. Гарячі ключі, єдина база, синхронні виклики зовнішніх систем.

Пастки

  • Почати з технологій («візьмемо Kafka»), а не з вимог.
  • Будувати під мільярд користувачів, коли їх тисяча: інтерв’юер спитає навіщо.
  • Застрягти в одному компоненті й не показати систему цілком.

Правило

Кожен блок на схемі пояснюється вимогою або цифрою з оцінки.

Що насправді оцінюють

  • Роботу з невизначеністю. Припущення фіксують уголос і пишуть на дошці: «10 млн користувачів на день, читання до запису 100:1».
  • Висновки з цифр. 2 тис. RPS на читання витримає одна база з кешем, 200 тис. — уже ні. Архітектура випливає з оцінки, а не з моди.
  • Глибину на запит. Інтерв’юер сам обирає, де копати: сховище, відмови, узгодженість. Готуватися треба до будь-якого напряму.

Ціна

Кожен компонент на схемі — це затримка, гроші й ще одна точка відмови. Черга згладжує піки, але додає асинхронність і повторну доставку. Кеш знімає навантаження, але потребує інвалідації.

Компроміси

Називайте явно, чим жертвуєте: узгодженістю заради доступності, свіжістю заради затримки, простотою заради масштабу. Відповідь «залежить від вимог» зараховують лише з продовженням «…і ось від яких».

Як перевірити в продакшені

Завершують спостережуваністю: які метрики та SLO покажуть, що система справляється (p99, глибина черг, частка помилок), і що зламається першим, коли навантаження зросте в 10 разів.

Підсумок: на senior-рівні оцінюють не схему, а ланцюжок «вимога → цифра → рішення → ціна» і вміння його захистити.

Оцінка на серветці: сервіс коротких посилань
Припущення: 100 млн нових посилань на місяць, читання до запису 100:1,
~500 байт на посилання, пік = 5 × середнє. Доба ≈ 86 400 с ≈ 10^5 с.
1. Запис: 100 млн / 30 / 86 400 с ≈ 40 RPS, пік ≈ 200 RPS
2. Читання: 40 RPS × 100 ≈ 4 000 RPS, пік ≈ 20 000 RPS
3. Дані: 100 млн × 500 байт ≈ 50 ГБ на місяць ≈ 600 ГБ на рік
4. Кеш: 20% посилань за місяць
100 млн × 0,2 × 500 байт ≈ 10 ГБ — уміщується в пам'ять одного вузла
5. Висновок: запис витримає одна база, читання — кеш перед нею;
за 5 років ≈ 3 ТБ — на той час подумати про шардування
Middle+

Як масштабувати вебсистему?

Section titled “Як масштабувати вебсистему?”
Відповідь

Що це

Спосіб Як Межа
Вертикальне потужніший сервер: більше CPU й пам’яті найбільша машина, простій під час заміни
Горизонтальне більше серверів за балансувальником майже немає, але сервіси мають бути stateless

Навіщо stateless

Балансувальник надсилає запити одного користувача на різні сервери. Якщо кошик зберігається в пам’яті сервера A, на сервері B його немає.

Як користуватися

  • Сесію — у розподілений кеш (Redis) або в токен.
  • Завантажені файли — в об’єктне сховище (Azure Blob Storage, S3).
  • Статику — у CDN, ближче до користувача.

Типова помилка

Увімкнути sticky sessions замість винесення стану. Коли сервер падає, користувачі втрачають сесії, а навантаження розподіляється нерівномірно.

Як влаштовано

Рівні на шляху запиту:

  1. CDN віддає статику й кешовані відповіді з точки присутності поруч із користувачем.
  2. Балансувальник L4 розподіляє TCP-з’єднання за IP і портом, не зазираючи в HTTP. L7 бачить шлях, заголовки й cookie, маршрутизує за URL і завершує TLS.
  3. Stateless-сервіси — N однакових екземплярів з автомасштабуванням за CPU або RPS.
  4. Кеш у пам’яті й розподілений — докладно в темі Фонові задачі й кешування.
  5. База. Репліки читання — для запитів, які терплять відставання; шардування — коли не справляється запис. Докладно — у темі Проєктування й експлуатація.
  6. Черга — для роботи, яку можна зробити пізніше: листи, звіти, інтеграції.

Коли що обирати

Вузьке місце Рішення
CPU сервісів більше екземплярів
читання з бази кеш, потім репліки
запис у базу партиціювання, потім шардування
короткі піки черга й фонові обробники

Пастки

  • Лаг репліки. Користувач зберіг профіль і не бачить змін. Своє після запису читають із первинної бази.
  • З’єднання з базою. 50 екземплярів × пул на 100 з’єднань — до 5 000 підключень до одного сервера.

Правило

Масштабують те, що вперлося за метриками. Шардування — останнім: воно найдорожче в розробці.

Під капотом

  • Пропускна здатність системи дорівнює пропускній здатності найвужчого спільного ресурсу. Спільна база, глобальне блокування чи один зовнішній провайдер обмежують систему, хоч скільки буде екземплярів.
  • L4 проти L7. L4 дешевший і не залежить від протоколу, але не бачить запитів: довге HTTP/2- чи WebSocket-з’єднання лишається на одному сервері. L7 розбирає HTTP, розподіляє окремі запити й перевіряє здоров’я за ендпоінтом. В Azure це Load Balancer (L4), Application Gateway (L7 у регіоні) і Front Door (глобальний L7 з CDN).
  • Ключ шардування обирають так, щоб майже кожна операція жила в одному шарді: магазин, клієнт, регіон.

Ціна

  • Кеш — застарілі дані, інвалідація й cache stampede під час масового закінчення терміну.
  • Репліки — лаг: асинхронна реплікація віддає старі дані, синхронна збільшує затримку запису.
  • Черга — затримка обробки, повторна доставка й вимога ідемпотентних обробників.

Компроміси

Автомасштабування сервісів без обмеження конкурентності переносить пік на базу: 10 екземплярів замість 2 — уп’ятеро більше одночасних запитів до неї. Тому пік приймає черга, а кількість обробників обмежують можливостями бази чи провайдера.

Як перевірити в продакшені

Кожен рівень має свою метрику: RPS і p99 на екземпляр, hit ratio кешу, лаг реплік, глибина черги й вік найстарішого повідомлення, кількість з’єднань із базою. Ємність підтверджують навантажувальним тестом до піку, а не під час нього.

Підсумок: масштабування — послідовне зняття вузьких місць; кожне рішення купує пропускну здатність ціною узгодженості, затримки чи складності.

Схема й кількість екземплярів
Користувач
→ CDN: статика й кешовані GET
→ балансувальник L7: TLS, маршрут за шляхом, перевірки здоров'я
→ API × N екземплярів: stateless, сесія в Redis
→ Redis: кеш і сесії
→ база: первинна для запису + репліки для читання
→ черга → обробники × M: листи, звіти, інтеграції
Скільки екземплярів API:
пік 6 000 RPS, за навантажувальним тестом один екземпляр тримає ~800 RPS за нормального p99
6 000 / 800 ≈ 8, плюс запас на відмову зони й деплой → 10–12
Middle+

Що таке CAP, PACELC та eventual consistency і як це впливає на проєктування?

Section titled “Що таке CAP, PACELC та eventual consistency і як це впливає на проєктування?”
Відповідь

Що це

Дані зберігаються в кількох копіях — репліках — на різних серверах. Поки копія наздоганяє основну, клієнти можуть бачити різні значення.

  • Узгодженість (C) — кожне читання бачить останній запис.
  • Доступність (A) — кожен робочий вузол відповідає на запит.
  • Стійкість до розділення (P) — система працює, коли мережу між вузлами розірвано.

Як це проявляється

Зв’язок між дата-центрами зник. Вузол може відмовити в запиті й зберегти C або відповісти тим, що знає, і зберегти A — можливо, зі старими даними.

Типова помилка

Казати «обираємо CA». Мережа рветься незалежно від нашого бажання, тому розподілена система обирає лише між C та A на час розриву.

Як влаштовано

  • CAP. C тут — лінеаризовність: система поводиться як одна копія, і завершений запис бачать усі наступні читання. A — кожен запит до робочого вузла отримує відповідь.
  • PACELC. Якщо розділення (P) — вибір між A та C; інакше (E) — між затримкою (L) і C. За Abadi, Dynamo, Cassandra й Riak за замовчуванням PA/EL, а HBase і VoltDB — PC/EC.
  • Eventual consistency. Репліки сходяться, якщо нові записи припинилися. Термін сходження не гарантовано.
  • Read-your-writes. Автор одразу бачить свої зміни, навіть якщо інші поки що ні.
  • Кворум. N реплік, запис чекає W підтверджень, читання опитує R реплік. Якщо W + R > N, читання зачіпає хоча б одну репліку з останнім записом.

Коли що обирати

Операція Вибір
Платіж, списання залишку лінеаризовність: запис через лідера або транзакцію, під час розділення — відмова
Кошик доступність: прийняти додавання завжди, конфлікти злити
Стрічка, лічильник переглядів eventual, автору — read-your-writes

Пастки

  • Вважати eventual «майже миттєвою». Під навантаженням лаг репліки — секунди й більше.
  • Чекати лінеаризовності від кворуму. За паралельних записів і збоїв посеред запису її немає.

Правило

Рівень узгодженості обирають для кожної операції, а не для всієї системи.

Під капотом

  • Доведення Gilbert і Lynch стосується одного об’єкта в асинхронній мережі. Про транзакції, затримки й часткову доступність воно нічого не каже.
  • Kleppmann у «Designing Data-Intensive Applications» радить не ділити бази на CP і AP: визначення надто вузькі, а реальні системи налаштовуються по-різному для різних операцій.
  • Abadi наголошує: без розділень CAP не заважає дати і ACID, і високу доступність. Слабку узгодженість у звичайній роботі обирають заради затримки, тобто за ELC.
  • Навіть за W + R > N Dynamo-подібні системи не дають лінеаризовності: заважають паралельні записи, sloppy quorum і частково застосовані записи.

Ціна

  • Лінеаризовність потребує координації: запис чекає лідера або більшості реплік. Між регіонами це десятки мілісекунд на кожну операцію.
  • Eventual consistency перекладає роботу на застосунок: розв’язання конфліктів (last-write-wins мовчки втрачає дані, злиття складніше), ідемпотентність, компенсації.

Компроміси

Сесійні гарантії — read-your-writes, monotonic reads — дешева середина: користувач бачить узгоджену картину своїх дій без глобальної координації. В Azure Cosmos DB це рівень Session, він за замовчуванням; усього рівнів п’ять: Strong, Bounded staleness, Session, Consistent prefix, Eventual.

Як перевірити в продакшені

Лаг реплік і час сходження — метрики з алертами. Аномалії шукають тестами з внесенням збоїв у дусі Jepsen: розділяють мережу й перевіряють історію операцій на порушення гарантій.

Підсумок: узгодженість — спектр, а не перемикач; її обирають під операцію, платячи затримкою за сильні гарантії та складністю застосунку за слабкі.

Кворуми й поведінка під час розділення
N = 3 репліки
W = 2, R = 2 → W + R = 4 > 3: читання зачіпає репліку з останнім записом;
переживає втрату 1 репліки і для читання, і для запису
W = 3, R = 1 → швидке читання, але запис зупиняється при втраті будь-якої репліки
W = 1, R = 1 → мінімальна затримка, eventual: читання може повернути старе
Розділення: вузол A бачить лише себе, вузли B і C — один одного
вибір C: A відмовляє в записі (немає кворуму), B і C працюють далі
вибір A: A приймає запис, після відновлення мережі — розв'язання конфлікту
Senior+

Навіщо потрібен API Gateway і що він має й не має робити?

Section titled “Навіщо потрібен API Gateway і що він має й не має робити?”
Відповідь

Що це

API Gateway — сервіс, через який проходять усі запити клієнтів до системи. Клієнт звертається до api.example.com/orders, а шлюз знає, що це сервіс замовлень.

Навіщо

  • Клієнт знає одну адресу, а не десяток сервісів.
  • Спільні задачі — TLS, перевірка токена, ліміти запитів, логи — виконуються в одному місці.
  • Внутрішні сервіси не відкриті в інтернет.

Як користуватися

Шлюз налаштовують маршрутами «шлях → група екземплярів сервісу»: наприклад, /orders/... → три екземпляри сервісу замовлень. Як шлюз і сервіси перевіряють JWT — у темі JWT.

Типова помилка

Писати в шлюзі бізнес-логіку: «якщо сума замовлення більша за 1000 — викликати перевірку». Шлюз пропускає й захищає запити, а рішення ухвалюють сервіси.

Як влаштовано

  • Маршрутизація за шляхом, хостом чи заголовком; балансування між екземплярами й перевірки їхнього здоров’я.
  • Автентифікація на краю. Запити без валідного токена відсікаються до сервісів.
  • Rate limiting і квоти за клієнтом або ключем API — захист від одного галасливого клієнта.
  • Агрегація. Один запит клієнта → кілька викликів сервісів → одна відповідь.
  • Наскрізне трасування. Шлюз починає або продовжує трасу за заголовком traceparent (W3C Trace Context), і запит видно від входу до бази.
  • Версіонування. /v1 і /v2 ведуть у різні версії сервісу; стару знімають, коли за метриками її перестали викликати.

Коли що обирати

Варіант Коли
YARP шлюз як .NET-застосунок: своя логіка в коді, свій хостинг
Azure API Management API для партнерів: портал розробника, ключі, квоти, політики без коду
Kong готовий шлюз на плагінах, незалежно від стеку сервісів

Пастки

  • Обхід шлюзу. Шлюз перевірив токен, а сервіс довіряє заголовку й доступний напряму — захисту немає.
  • Ліміт на екземпляр. Rate limiter ASP.NET Core рахує в пам’яті: на трьох екземплярах шлюзу ліміт фактично втричі вищий.
  • Агрегація. Відповідь чекає найповільніший сервіс, а його відмова ламає всю відповідь, якщо не передбачено часткової.

Правило

Шлюз знає про протокол і політики, але не про предметну область.

Під капотом

  • YARP — бібліотека поверх ASP.NET Core. Маршрути й кластери перезавантажуються з конфігурації без рестарту; є балансування (за замовчуванням PowerOfTwoChoices), активні й пасивні перевірки здоров’я, transforms. Автентифікація, rate limiting, output caching і таймаути — звичайні політики ASP.NET Core, призначені маршруту.
  • Azure API Management — керований шлюз із політиками на кшталт validate-jwt, rate-limit-by-key і quota та порталом для зовнішніх розробників.
  • Kong побудований на Nginx і OpenResty, можливості додають плагінами.

Ціна

  • Зайвий мережевий перехід і обробка на кожному запиті.
  • Єдина точка відмови: коли шлюз падає, недоступне все. Потрібні кілька екземплярів у різних зонах, плавна зупинка й запас під пік, а не під середнє навантаження.
  • Організаційна ціна: спільна конфігурація маршрутів — спільний релізний цикл.

Компроміси

  • Тонкий шлюз і BFF. Агрегацію під конкретного клієнта виносять у BFF — окремий бекенд для мобільного застосунку чи вебу, яким володіє команда цього клієнта. Шлюз лишається спільним і тонким.
  • Шлюз і service mesh. Шлюз обслуговує north-south трафік. Виклики між сервісами (east-west) mesh (Istio, Linkerd) закриває sidecar-проксі: mTLS, повтори, телеметрія без зміни коду. Ганяти внутрішні виклики через центральний шлюз — зайвий перехід і спільна точка відмови.

Як перевірити в продакшені

  • Метрики шлюзу за маршрутами: RPS, p99, частка 4xx і 5xx, відмови лімітера (429).
  • Тест обходу: прямий запит до сервісу повз шлюз має отримати відмову.

Підсумок: шлюз окупається, коли централізує політики краю, і шкодить, коли в ньому з’являються бізнес-правила; для внутрішнього трафіку є mesh, для потреб клієнта — BFF.

Шлюз на YARP: версія в шляху, ліміт і перевірки здоров'я
Yarp.ReverseProxy
using System.Threading.RateLimiting;
using Yarp.ReverseProxy.Configuration;
using Yarp.ReverseProxy.Transforms;
var builder = WebApplication.CreateBuilder(args);
// 100 запитів на секунду з одного IP — на кожному екземплярі шлюзу окремо
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.AddPolicy("per-client", ctx => RateLimitPartition.GetFixedWindowLimiter(
ctx.Connection.RemoteIpAddress?.ToString() ?? "unknown",
_ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromSeconds(1) }));
});
builder.Services.AddReverseProxy().LoadFromMemory(
[new RouteConfig
{
RouteId = "orders-v1",
ClusterId = "orders",
RateLimiterPolicy = "per-client",
Match = new RouteMatch { Path = "/v1/orders/{**rest}" },
}.WithTransformPathRemovePrefix("/v1")], // сервіс отримує /orders/...
[new ClusterConfig
{
ClusterId = "orders",
LoadBalancingPolicy = "RoundRobin",
// Кожні 10 с GET /health; хворий екземпляр виводиться з ротації
HealthCheck = new HealthCheckConfig
{
Active = new ActiveHealthCheckConfig { Enabled = true, Interval = TimeSpan.FromSeconds(10), Path = "/health" },
},
Destinations = new Dictionary<string, DestinationConfig>
{
["orders-1"] = new() { Address = "https://orders-1.internal/" },
["orders-2"] = new() { Address = "https://orders-2.internal/" },
},
}]);
var app = builder.Build();
app.UseRateLimiter(); // зайві запити отримують 429 на шлюзі й не доходять до сервісу
app.MapReverseProxy();
app.Run();
Senior+

Спроєктуйте сервіс масових сповіщень (SMS, e-mail, push) з піковими кампаніями.

Section titled “Спроєктуйте сервіс масових сповіщень (SMS, e-mail, push) з піковими кампаніями.”
Відповідь

Що це

Сервіс, який розсилає повідомлення великій кількості отримувачів: коди підтвердження, статуси замовлень, рекламу. Канали — SMS, e-mail і push.

Як влаштовано

  1. API приймає заявку: шаблон, список отримувачів, час надсилання.
  2. Черга зберігає повідомлення, доки їх не надішлють.
  3. Відправники беруть повідомлення з черги й передають провайдеру: SMS-шлюзу, поштовому сервісу, FCM або APNs.
  4. Статуси — надіслано, доставлено, помилка — зберігаються в базу й потрапляють у звіт.

Типова помилка

Надсилати просто в обробнику HTTP-запиту. Кампанія на 100 тисяч отримувачів не вкладеться в таймаут, а збій провайдера втратить частину повідомлень.

Як влаштовано

Шлях повідомлення:

  1. Приймання й валідація. Формат номерів і адрес, ліміти клієнта, наявність шаблону.
  2. Розгортання. Фонова задача перетворює кампанію на окремі повідомлення з підставленими даними отримувача.
  3. Черги з пріоритетами. Транзакційні повідомлення (коди, статуси замовлень) і маркетингові — у різних чергах. Код підтвердження не чекає на мільйон рекламних SMS.
  4. Надсилання. Пул відправників на канал і провайдера з обмеженням швидкості.
  5. Статуси. Провайдер підтверджує приймання одразу, а доставку — пізніше, колбеком або delivery receipt. Повідомлення проходить шлях Queued → Sent → Delivered або Failed.

Пастки

  • Повтори. Таймаут відповіді провайдера не означає, що повідомлення не пішло. Повтор без перевірки — подвійна SMS.
  • Часові пояси. Реклама «о 10:00» — за часом отримувача, а не сервера; уночі маркетинг не надсилають.
  • Довжина SMS. Кирилиця кодується в UCS-2: 70 символів в одному SMS, у складеному — 67 на сегмент. Вартість і ліміти провайдера зазвичай рахують у сегментах.

Правило

Кожне повідомлення має стабільний id від створення до фінального статусу: за ним дедуплікація, повтори й звірка статусів.

Під капотом

  1. Campaign API — приймання, валідація, розклад, ідемпотентний ключ заявки.
  2. Планувальник розгортає кампанію пачками з урахуванням часового поясу отримувача й тихих годин.
  3. Черги за каналом і пріоритетом. Гарантії доставки та outbox — у темі Черги й Kafka.
  4. Відправники тримають token bucket на кожного провайдера: TPS за договором і кількість SMPP-сесій. На відповідь ESME_RTHROTTLED швидкість знижують.
  5. Маршрутизатор провайдерів — ваги, вартість за напрямком, failover за часткою помилок через circuit breaker; патерни стійкості — у Стилях інтеграції.
  6. Приймач статусів приймає HTTP-колбеки та deliver_sm через SMPP і пов’язує їх із повідомленням за id провайдера.

Ціна

  • Відписку перевіряють безпосередньо перед надсиланням: від створення кампанії могли минути години. Для e-mail Gmail із 2024 року вимагає від масових відправників SPF, DKIM, DMARC і відписки в один клік.
  • Колбек надходить двічі або раніше, ніж записано id провайдера, — приймач ідемпотентний і вміє відкласти обробку.
  • Failover дублює повідомлення, якщо перший провайдер насправді його доставив.

Компроміси

Статуси — мільйони записів за кампанію. Гарячі дані тримають у таблиці з партиціями за днем і пишуть пачками, історію виносять в архів.

Як перевірити в продакшені

Глибина черги й вік найстарішого повідомлення за пріоритетом, фактичний TPS проти ліміту, частка помилок і час до доставки за провайдером, частка повідомлень без фінального статусу через добу.

Підсумок: сервіс сповіщень — це керування чужою пропускною здатністю: пріоритети, ліміти на провайдера, failover та ідемпотентність за id повідомлення.

Оцінка навантаження
Припущення: 5 млн отримувачів, маркетингова кампанія раз на день на всіх;
транзакційних SMS 1 млн на добу; кирилиця — зазвичай 2 сегменти;
2 провайдери по 500 сегментів/с
1. Транзакційні: 1 млн / 86 400 с ≈ 12 повідомлень/с, пік ×5 ≈ 60/с
2. Кампанія: 5 млн × 2 сегменти = 10 млн сегментів
3. Пропускна здатність: 2 × 500 = 1 000 сегментів/с,
з них 100/с — резерв під транзакційні
4. Час кампанії: 10 млн / 900 сегментів/с ≈ 11 000 с ≈ 3 години
→ коротший текст (1 сегмент), хвилі за часовими поясами
або підвищення ліміту в провайдера
5. Статуси: ~2 події на повідомлення × 5 млн ≈ 10 млн записів,
~200 байт кожен ≈ 2 ГБ за кампанію
Схема компонентів
Клієнти: CRM, сервіс замовлень, авторизація
→ Campaign API: валідація, ідемпотентний ключ заявки, розклад
→ Планувальник: розгортає кампанію пачками за часовим поясом
→ Черги: sms.transactional | sms.marketing | email | push
→ Відправники: перевірка відписки, дедуплікація за id, token bucket на провайдера
→ Маршрутизатор: SMPP-провайдер A | HTTP API провайдера B | пошта | FCM | APNs
← Приймач статусів: deliver_sm (delivery receipt), HTTP-колбеки
→ оновлення статусу за id провайдера
→ Сховище: повідомлення й статуси (партиції за днем), відписки
→ Метрики й алерти: глибина черг, TPS, помилки за провайдером
Вузькі місця: ліміт провайдерів, запис статусів, розгортання великих кампаній