
Для користувача відстеження посилки виглядає просто: ввів номер — побачив, де вона. Для розробника це значно складніша історія. За одним рядком “In transit” можуть стояти кілька перевізників, різні API, webhooks, черги подій, повторні запити, мапінг статусів і дані, що надходять із затримкою.
Міжнародна посилка — це гарний приклад розподіленої системи. Вона фізично рухається між складами та країнами, а цифровий слід збирається з незалежних джерел. Ці два процеси майже ніколи не синхронізовані ідеально.
Розберімося, що відбувається між сканером на складі та повідомленням у застосунку.

Tracking number — це ключ, а не GPS
Найпоширеніша помилка — сприймати tracking number як координати в реальному часі. Насправді він радше є ключем до набору подій.
Посилку відсканували під час приймання — з’явилася подія. Вона пройшла сортувальний центр — ще одна. Її передали іншому оператору — наступна.
Між цими точками коробка може проїхати сотні кілометрів без жодної нової цифрової події. Тому «статус не змінювався 12 годин» не означає, що вантаж стоїть на місці.
У найпростішому варіанті система зберігає tracking ID, перевізника, тип події, час, локацію, сирий і нормалізований статус. Справжні складнощі починаються, коли перевізник не один.
Один маршрут — кілька API
У США посилку може перевозити один оператор. Після прибуття на форвардинговий склад вона стає частиною міжнародного відправлення, а в Україні останню милю може виконувати інша служба.
Кожен учасник ланцюга має власну модель даних. Один повертає `delivered`, інший — `DEL`, третій — текстове `Package delivered to recipient`.
Якщо показувати користувачу сирі значення, інтерфейс швидко перетвориться на словник кодів.
Тому між зовнішніми API та frontend потрібен шар нормалізації. Десятки зовнішніх подій можна звести до кількох станів: `Created`, `Accepted`, `In transit`, `At warehouse`, `Customs`, `Out for delivery`, `Delivered`, `Exception`.
Саме мапінг часто визначає, чи зрозуміє користувач, що відбувається.
Webhook чи polling
Ідеальний сценарій — перевізник надсилає webhook одразу після нової події. Система приймає payload, перевіряє його, записує подію та оновлює агрегований статус.
Але не всі інтеграції однаково зручні. Десь є webhooks, десь тільки REST API, а партнерський інтерфейс може мати rate limits. Тоді доводиться використовувати polling.
Наївний варіант — кожні п’ять хвилин перевіряти всі tracking numbers. На сотнях відправлень це ще працює. На великих обсягах більшість запитів не приноситиме нових даних.
Краще частоту опитування прив’язувати до стану. Новостворений label можна перевіряти рідше. `Out for delivery` — частіше. Доставлені відправлення взагалі прибрати з активного polling.
Події приходять не по порядку
Уявімо, що на сортувальному центрі створено подію о 10:02, а наступний scan відбувся о 10:17. Через мережеву затримку друга подія потрапила у вашу систему першою.
Якщо просто оновлювати поле `status` у порядку HTTP-запитів, можна відкотити посилку назад у часі.
Тому важливо розділяти щонайменше два timestamp: `event_time` — коли подія сталася, і `received_at` — коли система її отримала.
Так само треба бути готовим до дублювання. Webhook може прийти двічі, партнер повторить запит після timeout, а polling витягне подію, яку система вже отримала іншим каналом.
Без idempotency одна фізична операція легко перетворюється на декілька push-повідомлень.
Idempotency рятує не тільки платежі
Для tracking events корисно мати зовнішній event ID або власний deduplication key — наприклад, комбінацію номера, типу події, часу та локації.
Це особливо важливо, якщо на статус підписані інші процеси.
Подія `ArrivedAtWarehouse` може оновити кабінет клієнта, створити push, поставити задачу складу та змінити список посилок, доступних для консолідації.
Якщо виконати весь ланцюг двічі, наслідки вже не косметичні.
Eventual consistency — це нормально
Користувачу хочеться, щоб фізичний світ і застосунок були синхронні до секунди. У логістиці це майже недосяжно.
Сканер може працювати офлайн. API партнера може віддавати дані із затримкою. Webhook може потрапити в retry queue. Застосунок може кешувати попередній стан.
Тому система зазвичай eventually consistent: через певний час усі компоненти сходяться до правильного стану, але в конкретну секунду можуть бачити різні версії реальності.
Проблема не в eventual consistency. Проблема — коли продукт її не пояснює.
Замість категоричного «Посилка знаходиться на складі X» іноді чесніше показати «Останнє оновлення: 14:32». Це одразу задає правильне очікування.
Черга між інтеграцією та бізнес-логікою
Якщо webhook запускає всю бізнес-логіку синхронно, зовнішній партнер фактично керує часом відповіді вашого endpoint.
Надійніша схема виглядає приблизно так:
carrier → webhook endpoint → validation → queue → event processor → database → notifications
Endpoint швидко приймає валідну подію. Нормалізація, дедуплікація, перерахунок стану та push виконуються асинхронно.
Це дає retry, backpressure і простіший контроль помилок. Для проблемних payload корисний dead-letter queue: вони не зникають і не блокують основний потік.
Чому «створено етикетку» може висіти кілька днів
Класичний кейс: магазин створив shipping label і передав tracking number покупцю. API перевізника вже знає номер, тому система показує `Label created`.
Але самої коробки у перевізника ще немає.
Магазин може передати її ввечері, наступного дня або після вихідних. Перший фізичний scan з’явиться лише під час приймання.
З технічного погляду все працює правильно. З продуктового — користувач бачить номер і думає, що доставка вже почалась.
Тому статуси краще називати мовою дії: «Магазин створив етикетку, очікуємо передачу перевізнику» точніше, ніж абстрактне «Відправлено».
Спостережуваність потрібна не менше, ніж трекінг
Якщо система агрегує кілька carrier API, простого 500 error у логах недостатньо.
Корисно відстежувати затримку між `event_time` і `received_at`, кількість номерів без оновлень довше встановленого порога, частоту 429 та 5xx від партнерів, розмір retry queue і кількість статусів, для яких ще немає мапінгу.
Особливо показовий lag. Якщо затримка подій одного партнера раптом зросла з кількох хвилин до сорока, користувачі ще можуть нічого не помітити, але інцидент уже почався.

UX — останній шар архітектури
Можна ідеально побудувати ingestion pipeline й усе одно отримати сотні звернень «де моя посилка?»
Причина проста: користувачу не потрібні ваші event types. Йому потрібна відповідь на три питання: що вже сталося, де ми зараз і що буде далі.
У міжнародній доставці шлях покупки зі США може включати магазин, американського carrier, склад, авіа чи море, митний етап і локальну доставку. Завдання продукту — перетворити цю складність на один зрозумілий маршрут.
Як приклад користувацького сценарію можна подивитися, як такий маршрут організований у Dnipro LLC: американська адреса, особистий кабінет, міжнародне відправлення та трекінг об’єднуються в один процес.
Для розробника tracking посилки — хороший навчальний приклад distributed systems. Тут є зовнішні API, невпорядковані події, дублікати, retries, eventual consistency, мапінг станів і людина, яка все одно очікує одну просту відповідь.
І це найцікавіша частина задачі: система може бути складною всередині, але для користувача вона має виглядати очевидною.
