← Powrót na blog

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.

Masz podobny problem? Napisz do nas.

Umów konsultację