Architektura
Proč microservices?
Monolitická aplikace je jednodušší na vývoj, ale obtížnější na škálování a údržbu. Microservices rozdělují systém na nezávisle nasaditelné jednotky — každá má vlastní zodpovědnost, vlastní proces a vlastní technologický stack.
Tři principy za tímto projektem:
Separation of concerns. contacts-cpp řeší persistence, search-rust řeší vyhledávání, gateway-go řeší discovery. Žádná služba nezná interní implementaci jiné — komunikují pouze přes HTTP/JSON kontrakty.
Polyglot persistence a jazyk. Každý problém si vybere nejvhodnější nástroj. Vyhledávání potřebuje rychlý in-memory index → Rust. CRUD s transakcemi → C++ s libpqxx. Orchestrace a proxy → Go stdlib. UI a lepidlo → Python/FastAPI.
Nezávislé škálování. Kdybychom search-rust dostali pod větší zátěž, nasadíme více instancí bez dotyku ostatních služeb. V tomto projektu to neřešíme (jeden Docker host), ale architektura to umožňuje.
BFF pattern (Backend for Frontend)
bff-python implementuje pattern Backend for Frontend — jeden agregační backend navržený specificky pro jednoho klienta (webový prohlížeč).
Bez BFF by prohlížeč musel volat tři různé backendy (contacts-cpp, search-rust, gateway-go), řešit CORS, znát jejich adresy a agregovat výsledky sám. BFF tuto komplexitu skryje za jednu adresu :8989.
Klíčová vlastnost: /api/stats volá tři backendy paralelně přes asyncio.gather. Výsledná latence je max(t1, t2, t3), ne t1 + t2 + t3. Bez asyncio by stejný endpoint trval 3× déle.
Service Registry pattern
gateway-go implementuje Service Registry — centrální katalog kde běžící služby. Každá služba se při startu zaregistruje (POST /services) a gateway ji od té chvíle pravidelně pinguje (GET /health každých 10 s) a měří latenci.
Alternativou by bylo statické nastavení adres v config souboru. Registry přidává dynamiku — pokud kontejner padne a nastartuje na jiné adrese, stačí se znovu zaregistrovat. V tomto projektu jsou adresy statické (Docker Compose DNS), ale pattern zůstává stejný jako v produkčních systémech (Consul, etcd).
Tok dat
Prohlížeč
│ HTTP :8989
▼
bff-python ← jediný vstupní bod; servuje HTML a přeposílá API volání
├── /api/contacts ──► contacts-cpp :8080 (CRUD)
├── /api/search ──► search-rust :8081 (fulltextový index)
├── /api/services ──► gateway-go :9000 (registry)
├── /api/topology ──► gateway-go :9000
└── /api/stats ──► všechny 3 najednou (asyncio.gather)
contacts-cpp ──► search-rust (POST /index při každé změně dat)
contacts-cpp ──► postgres :5432 (persistentní úložiště)
všechny služby ──► gateway-go (registrace při startu)
CAP theorem v tomto systému
CAP theorem říká, že distribuovaný systém může garantovat současně nejvýše dvě ze tří vlastností: Consistency (všichni vidí stejná data), Availability (systém vždy odpovídá) a Partition tolerance (systém funguje i při výpadku sítě mezi uzly).
Polycontacts volí AP — dostupnost nad konzistencí:
contacts-cppzapíše kontakt do PostgreSQL a asynchronně notifikujesearch-rust- V okně mezi zápisem a doručením notifikace vrátí search stará data
- Systém preferuje odpovědět rychle a možná trochu zastarale před čekáním na plnou synchronizaci
Tato forma nekonzistence se nazývá eventual consistency — systém se časem dostane do konzistentního stavu, ale nezaručuje to okamžitě. Pro adresář kontaktů je to přijatelný kompromis.
Fire-and-forget pattern
contacts-cpp po každém POST/PUT spustí detached thread který pošle POST /index na search-rust. Thread není nijak sledován — pokud selže (search-rust je dole, síť je přetížená), chyba se zahodí.
POST /contacts
│
├── uloží do PostgreSQL (synchronně, musí uspět)
└── spustí detached thread → POST /index na search-rust (asynchronně, best-effort)
│
└── chyba? zahodí se. Žádný retry, žádný log.
Výhoda: zápis kontaktu je rychlý, neblokuje ho dostupnost search-rust. Nevýhoda: search index může zaostávat za databází.
V produkčních systémech se tento pattern řeší message queue (Kafka, RabbitMQ) — zpráva se uloží durably a search-rust ji zpracuje až bude ready. My záměrně používáme jednodušší přístup.
Co gateway NENÍ
Gateway-go není na datové cestě pro čtení a zápis kontaktů. BFF volá contacts-cpp a search-rust přímo — gateway by přidala zbytečný network hop a latenci.
Gateway slouží jako service registry: ví, které služby běží, na jakých adresách a jestli jsou zdravé. Prohlížeč se může zeptat přes /api/services a vidět live stav celého systému. Reverse proxy v gateway existuje jako bonus — umožňuje volat backend přímo přes gateway URL (/contacts-cpp/contacts) bez znalosti interní adresy. V produkci to BFF nepoužívá.
Startup sekvence
Docker Compose spouští služby v závislostním pořadí definovaném depends_on:
- postgres nastartuje první,
pg_isreadyhlídá připravenost - gateway-go nastartuje paralelně s postgres (nepotřebuje DB)
- contacts-cpp čeká na
postgres healthy+gateway-go healthy, pak se připojí k DB, vytvoří schema, vloží seed data a v detached vlákně se zaregistruje u gateway - search-rust čeká na
contacts-cpp healthy, pak v background tasku načte kontakty přesGET /contacts, zaindexuje je a zaregistruje se u gateway - bff-python čeká na
gateway-go healthy, při startu vytvoří sdílenýhttpx.AsyncClienta zaregistruje se u gateway
Meziservisní komunikace
| Volající | Cíl | Endpoint | Kdy |
|---|---|---|---|
contacts-cpp |
postgres |
libpqxx / TCP 5432 | Každý CRUD požadavek |
contacts-cpp |
search-rust |
POST /index |
Po každém POST/PUT kontaktu (detached thread, best-effort) |
contacts-cpp |
gateway-go |
POST /services |
Při startu (retry loop, 5 pokusů) |
search-rust |
contacts-cpp |
GET /contacts |
Při startu (retry loop do úspěchu) |
search-rust |
gateway-go |
POST /services |
Při startu, jednorázově |
bff-python |
contacts-cpp |
GET/POST/PUT/DELETE /contacts |
Na každý požadavek z prohlížeče |
bff-python |
search-rust |
GET /search |
Na každý vyhledávací požadavek |
bff-python |
gateway-go |
GET /services, GET /topology |
Na vyžádání z UI |
bff-python |
všechny 3 | GET /stats |
Paralelně při /api/stats |