Jak zbudowaliśmy Carivio: dyspozytornię dla 50 pojazdów od zera
Trójplatformowa platforma taxi dispatching w 4 miesiące. Backend 128 000 linii kodu, aukcja kursów w czasie rzeczywistym przez SignalR, PostGIS geospatial, produkcyjny Claude AI. Co było trudne i czego nie widać w demo.
Carivio to platforma taxi dispatching, którą zbudowaliśmy dla Transport Prague. Dziś jeździ na niej około 50 pojazdów. To nasz własny produkt — backend, aplikacja webowa dla dyspozytorów i aplikacja mobilna dla kierowców, zbudowane od zera.
Ten artykuł dotyczy decyzji architektonicznych, które podejmowaliśmy, oraz rzeczy, które są trudne w produkcji, ale których nikt nie widzi w demo.
Co to jest i co musi umieć
Taxi dispatching wygląda z zewnątrz prosto: klient zamawia kurs, kierowca go przyjmuje, wiezie klienta. W produkcji to automat stanów z dziesiątkami przejść, sześcioma rolami użytkowników i wymogiem, żeby każda akcja zachodziła w czasie rzeczywistym.
Platforma ma 6 ról: klient, kierowca, dyspozytor, porter (recepcjonista w hotelu), admin i gość. Każda rola widzi inny widok, ma inne uprawnienia i inne powiadomienia. Backend jest jeden, ale modułów funkcjonalnych jest 48. Migracji EF Core przybyło 107 — to dobry wskaźnik tego, jak bardzo model domenowy ewoluuje w trakcie projektu.
Model aukcyjny kursów
Największa decyzja architektoniczna dotyczył sposobu przydzielania kursu kierowcy.
Wybraliśmy model aukcyjny: gdy wpływa zamówienie, system rozsyła je grupie kierowców w zasięgu. Widzą ofertę na telefonie i mogą ją przyjąć. Kto przyjmie pierwszy (lub na najkorzystniejszych warunkach), dostaje kurs. Pozostałym oferta wygasa.
Alternatywa — bezpośrednie przydzielenie najbliższemu kierowcy — jest technicznie prostsza. Wymaga jednak albo sztywnej reguły, która może nie pasować do każdej sytuacji, albo dyspozytora podejmującego decyzje ręcznie. Aukcja daje kierowcom autonomię i dobrze działa w sytuacjach, gdy ktoś jest formalnie najbliżej, ale z różnych powodów nie jest zainteresowany.
Technicznie oznacza to: automat stanów po stronie backendu, broadcast oferty przez SignalR do wszystkich odpowiednich kierowców, powiadomienia push na wypadek gdy aplikacja nie jest na pierwszym planie, i job Quartz pilnujący timeouta i zamykający ofertę po upływie terminu. Stan każdego kursu jest utrwalony — jeśli serwer restartuje się w środku aukcji, system to przeżywa.
Real-time: cztery huby SignalR
Komunikacja w czasie rzeczywistym działa przez SignalR. Mamy cztery endpointy hub — jeden dla klientów, jeden dla kierowców, jeden dla dyspozytorów i jeden dla porterów.
Każdy hub ma inne grupy, inne zdarzenia i inne wymagania co do latencji. Dyspozytor widzi pozycje wszystkich pojazdów na mapie w czasie rzeczywistym. Klient otrzymuje powiadomienia o stanie kursu. Kierowca dostaje oferty i aktualizacje.
Nie wystarczy to zbudować i zapomnieć. Przy każdej zmianie modelu stanów trzeba przejść przez wszystkie huby i sprawdzić, czy każdy handler otrzymuje właściwe dane we właściwym momencie. Reference discovery po całym codebase to podczas rozwoju konieczność, nie luksus.
Geospatial: PostGIS
Pozycje GPS kierowców przechowujemy w PostgreSQL z rozszerzeniem PostGIS. PostGIS daje indeksy przestrzenne i zapytania geograficzne w SQL — „znajdź wszystkich kierowców w promieniu 5 km od punktu X" to jedno zapytanie, a nie logika aplikacyjna.
Do geokodowania (adres → współrzędne) i wyszukiwania miejsc używamy zewnętrznego API z warstwą HybridCache + Redis. Wyniki geokodowania są cache'owane — te same adresy pytamy raz, nie setki razy dziennie.
Backend nosi też własny silnik finansowy i fakturacyjny. Ruch taxi ma specyficzne wymagania rozliczeniowe, których pudełkowe rozwiązania nie pokrywają.
Co jest niewidoczne w demo: background location
Najbardziej skomplikowana część całej platformy nie tkwi w logice backendowej. Jest w aplikacji mobilnej po stronie Androida.
Background location — wysyłanie pozycji GPS do serwera nawet gdy aplikacja jest schowana w tle — to na papierze prosta funkcja. W produkcji to walka z producentami telefonów.
Samsung, Xiaomi/MIUI, OnePlus, Huawei — każdy ma własną agresywną optymalizację baterii, która zabija aplikacje działające w tle. Producenci OEM nazywają „zalecanym zachowaniem" to, co de facto jest zabitym procesem działającym w tle. Efekt: kierowca myśli, że jest online, ale pozycja GPS przestała napływać 10 minut temu.
Poradziliśmy sobie z tym na kilku poziomach:
- Wykrywanie środowiska OEM przy starcie aplikacji i wyświetlanie przewodnika ustawień dla konkretnego producenta
- Foreground service z trwałym powiadomieniem — na Androidzie jedyny niezawodny sposób, żeby przeżyć uśmiercenie przez baterię
- Wykrywanie zabicia aplikacji i recovery przy następnym uruchomieniu (niedokończone stany kursów, przywrócenie połączenia SignalR)
- Kolejka offline: jeśli telefon chwilowo nie ma zasięgu, punkty GPS zapisują się lokalnie i są wysyłane po przywróceniu połączenia
To nie brzmi jak duży projekt, ale debugowanie zachowania na rzeczywistych urządzeniach różnych producentów zajęło znaczną część czasu wytwarzania aplikacji mobilnej.
Produkcyjne AI: swobodne zlecenie → ustrukturyzowane zamówienie
Jedna z funkcji działających w systemie to przetwarzanie zamówień swobodnym tekstem przez Claude.
Dyspozytor lub admin wpisuje w pole cokolwiek — „jutro rano o 8 z Václavaka na lotnisko, 3 osoby, sedan" — a model zwraca ustrukturyzowany obiekt: typ pojazdu, liczba pasażerów, miejsce odbioru i cel, godzina, uwagi.
Implementacja ma po stronie backendu śledzenie kosztów (każde żądanie jest logowane z liczbą tokenów) i guardrails po stronie serwera: model dostaje schemat, który musi wypełnić, a jeśli zwróci niekompletny lub niespójny wynik, system to odrzuca i prosi o korektę. Halucynacje w adresach to realny problem — dyspozytor musi mieć możliwość sprawdzenia i poprawienia wyników zanim zamówienie trafi do kierowcy.
Liczby
- Backend: ~128 000 linii kodu
- Modułów funkcjonalnych: 48
- Migracji EF Core: 107
- Endpointy hub SignalR: 4
- Background joby Quartz: 12 (kontrola stanów kursów, timeouty aukcji, przypomnienia, czyszczenie offline kierowców)
- Aktywne pojazdy w produkcji: ~50
- Czas wytwarzania MVP: 4 miesiące
Co z tego wynieść
MVP w 4 miesiące było możliwe, bo zakres był jasny od początku, a decyzje architektoniczne zapadły w pierwszym tygodniu, nie w trakcie. Aukcja vs. bezpośrednie przydzielanie, SignalR vs. polling, PostGIS vs. aplikacyjny geospatial — to decyzje, które trudno zmienić późno.
Najkosztowniejsza część wytwarzania to nie była złożona logika biznesowa. Było to debugowanie background location na rzeczywistych telefonach różnych producentów i obsługa stanów, które w demo nigdy nie wystąpią, ale w produkcji pojawiają się każdego dnia.
Nie każdy projekt idzie w takim tempie. Zależy od tego, ile rzeczy zmienia się w trakcie i jak bardzo domenę trzeba odkrywać. Carivio miało tę przewagę, że zamawiający dobrze wiedział, czego chce.
Jeśli budujecie własny dispatching lub rozważacie digitalizację floty, napiszcie do nas — chętnie powiemy, co zrobilibyśmy inaczej i co ma sens w waszym przypadku.
FAQ
Jak długo trwało zbudowanie Carivio od zera?
MVP w 4 miesiące. Warunkiem był jasny zakres i decyzje architektoniczne podjęte na początku, nie w trakcie. Nie każdy projekt idzie w takim tempie — zależy od złożoności domeny i tego, ile rzeczy zmienia się po drodze.
Dlaczego wybraliście model aukcyjny kursów zamiast bezpośredniego przydzielania?
Bezpośrednie przydzielanie wymaga albo sztywnej reguły (najbliższy kierowca), albo dyspozytora podejmującego decyzje. Aukcja daje kierowcom autonomię i naturalnie radzi sobie z sytuacjami, gdy ktoś jest bliżej, ale nie jest zainteresowany. Technicznie oznacza to automat stanów i broadcast przez SignalR — bardziej złożone niż proste przydzielanie, ale elastyczniejsze operacyjnie.
Czy platforma obsłuży większą flotę niż 50 pojazdów?
Architektura jest na to gotowa. Indeksy przestrzenne PostGIS, HybridCache z Redisem i bezstanowy backend umożliwiają skalowanie horyzontalne. Rzeczywista pojemność zależy od ruchu i infrastruktury — wymagałoby to testu obciążeniowego z konkretnymi liczbami.