slotwise — бекенд бронювання прийомів
Огляд
Незалежний demo-проєкт: REST API на Go для бронювання прийомів — ресурси, послуги, життєвий цикл бронювання та Telegram-нагадування. Моделює патерн, що повторюється в широкому спектрі бізнесів (клініки, репетиторство, консультації, салони): ресурси бронюються на часові слоти, і єдине правило, яке ніколи не можна порушити — той самий ресурс не можна забронювати двічі.
Результат: гарантія проти подвійного бронювання живе в обмеженні Postgres, не в коді застосунку — і це твердження доведено живим тестом на конкурентність, а не просто заявлено.
Справжня складна задача
Очевидний підхід — перевірити на перетини, тоді вставити, якщо немає — має race window. Два запити на той самий ресурс з перетинним часом можуть обидва пройти перевірку до того, як хоч один вставить запис, і обидва вдадуться. Це реальний клас багів у системах бронювання, не гіпотетичний.
slotwise не робить “перевір-потім-встав”. Таблиця bookings має обмеження Postgres EXCLUDE на (resource_id, during) через btree_gist, де during — це tstzrange. Сам Postgres відхиляє другу вставку, якщо діапазони перетинаються — гарантія живе в тій самій транзакції, що й запис, тож для гонки просто немає вікна.
Архітектура

Ключові інженерні рішення
| Рішення | Чому |
|---|---|
EXCLUDE USING gist (resource_id WITH =, during WITH &&) замість app-level перевірки перетинів |
БД забезпечує це всередині тієї самої транзакції, що й вставка — жодного race window при конкурентних запитах, який завжди має app-level перевірка |
Помилка Postgres 23P01 (exclusion_violation) перекладена в типізовану ErrSlotTaken |
Шар хендлерів ніколи не має знати про коди помилок Postgres; саме доменна помилка поширюється до 409 Conflict |
| Переходи статусу бронювання перевіряються проти єдиної таблиці легальних ребер | Той самий патерн, що й стейт-машина нарядів у fleet-crm: pending → confirmed → completed/cancelled/no_show, нелегальні переходи відхиляються типізованою помилкою, не приймаються мовчки |
Відправка нагадування вимагає StatusConfirmed і “ще не відправлено”, перевіряється до виклику notifier |
Нагадування для непідтвердженого бронювання чи повторне сповіщення клієнта — реальні “footguns” — перевіряється на рівні сервісу, і невдалий виклик notifier ніколи не позначає нагадування як відправлене |
Інтерфейс Notifier з фолбеком LogNotifier |
API повністю запускається без жодних зовнішніх credentials — Telegram bot token не потрібен для білду, тестів чи демо |
Результати
- Повний REST API: JWT-авторизація, ресурси/послуги/бронювання, перевірений життєвий цикл бронювання, Telegram-нагадування
- Репозиторії та notifier на інтерфейсах всюди — кожен тест сервісу/хендлера працює проти in-memory fake;
go test ./... -raceпроходить чисто, БД не потрібна - Гарантія проти подвійного бронювання доведена, не просто побудована: два одночасні запити
POST /api/bookings, запущені паралельно проти живого сервера, на той самий ресурс з перетинним часом — підтверджено на живому бінарнику: рівно один201і один409, рівно один запис у БД - Перевірено наскрізно через
docker compose up: міграції застосовуються (включно зCREATE EXTENSION btree_gist), login засіяного користувача, повний CRUD, легальні/нелегальні переходи статусу, правила нагадувань (409 до confirm, 409 при повторній відправці)
Код: github.com/valpere/slotwise
Стек
Go · chi (router) · pgx/v5 (без ORM) · golang-jwt · bcrypt · PostgreSQL (tstzrange + EXCLUDE USING gist) · Telegram Bot API · Docker Compose · testify