Najlepsze praktyki projektowania API w architekturze mikroserwisów dla nowoczesnych aplikacji webowych

0
16
Rate this post

Spis Treści:

Rola API w architekturze mikroserwisów

API jako kontrakt, nie tylko transport

API w architekturze mikroserwisów pełni rolę kontraktu między serwisami i między backendem a front-endem. To nie jest tylko „rura HTTP”, ale precyzyjnie zdefiniowany zestaw reguł: jakie zasoby są dostępne, jakie operacje można na nich wykonać, jakie dane zwróci system oraz w jakich sytuacjach pojawią się błędy. Im jaśniejszy kontrakt API, tym łatwiej rozwijać system niezależnie, bez nieustannej koordynacji między zespołami.

Kontrakt API musi być zrozumiały nie tylko dla backendu, ale także dla front-endu, testerów, DevOpsów i analityków. Dlatego szczególnie przydatna staje się dokumentacja API w OpenAPI (dawniej Swagger), która jest traktowana jako „źródło prawdy” dla wszystkich uczestników projektu. Zapisany w jednym miejscu kontrakt można potem używać do generowania klientów, stubów, testów kontraktowych i dokumentacji dla biznesu.

Kluczowe jest też rozróżnienie, co jest elementem publicznego kontraktu API (widocznego na zewnątrz serwisu), a co jest szczegółem implementacyjnym. Publiczny kontrakt powinien być stabilny, przewidywalny i ewolucyjny. To, jak wewnętrznie serwis wylicza pola, przechowuje dane czy integruje się z innymi systemami, nie powinno „przeciekać” do kontraktu.

Konsekwencje złego projektu API

Zły projekt API w mikroserwisach szybko mści się na zespole. Najczęstszy efekt to nadmierne sprzężenie między serwisami: zmiana jednego kontraktu wymusza modyfikacje w kilku innych, wdrożenie wymaga koordynacji i wspólnego release’u, a mała zmiana biznesowa zamienia się w wielotygodniowy projekt.

Inny problem to krucha architektura, gdzie pojedyncza awaria lub spowolnienie jednego serwisu rozlewa się po całym systemie. Dzieje się tak, gdy kluczowe funkcje realizowane są w łańcuchu synchronicznych wywołań bez idempotentnych endpointów i bez wzorców ochronnych (circuit breaker, time-outy). Źle zaprojektowane API potrafi doprowadzić do efektu domina – lawiny retry, rosnących time-outów, blokad threadów i ostatecznie do utraty dostępności.

Do kompletu dochodzą problemy z wdrażaniem. Jeżeli API jest projektowane ad hoc, bez strategii wersjonowania i ewolucji, każda zmiana niesie ryzyko „breaking change” dla nieznanych klientów. Pojawia się strach przed deployem, statyczne zależności wersji, konieczność wspólnych okien serwisowych. Mikroserwisy zaczynają przypominać „rozparcelowany monolit”, w którym i tak wszystko trzeba koordynować.

API monolitu vs API wielu mikroserwisów

W monolicie API zazwyczaj jest jedną, spójną warstwą wystawiającą funkcjonalności całej aplikacji. W środku można pozwolić sobie na ciasne powiązania między modułami, bo są one ukryte w jednym procesie. W mikroserwisach każdy serwis jest osobnym systemem, z własnym API, cyklem życia, wdrożeniami i wersjonowaniem.

Najważniejsza różnica: w monolicie można szybko poprawić błąd w jednym miejscu, zrekompilować i wdrożyć wszystko na raz. W architekturze mikroserwisów wzajemne API musi być traktowane jak zewnętrzne: z zachowaniem kompatybilności wstecznej, świadomością opóźnień sieci, częściowych awarii i złożonej topologii wywołań. Zespół nie ma luksusu „podmienienia” wszystkich konsumentów w jednym sprintcie.

API mikroserwisów często dzieli się na dwa poziomy:

  • API zewnętrzne (publiczne) – konsumowane przez front-end, klientów mobilnych lub systemy partnerów.
  • API wewnętrzne – używane wyłącznie przez inne mikroserwisy w obrębie tej samej organizacji.

To rozróżnienie ma duże znaczenie dla wymagań stabilności, bezpieczeństwa i dopuszczalnych zmian. API zewnętrzne musi być znacznie bardziej konserwatywne i starannie wersjonowane, API wewnętrzne może ewoluować szybciej, przy lepszej koordynacji zespołów.

Miejsce API w cyklu życia funkcjonalności

Projektując mikroserwisy, warto traktować API jako element od początku do końca całego cyklu życia funkcjonalności. Zaczyna się od analizy biznesowej – definicji zdarzeń domenowych, przypadków użycia i kontekstów (bounded context). Na tej podstawie pojawia się projekt kontraktów: jakie zasoby powstaną, jakie operacje muszą być dostępne, jakie dane są naprawdę potrzebne przez front-end.

Następnie API staje się podstawą do testów kontraktowych między zespołami. Po wdrożeniu ruch jest monitorowany, a metryki API (czas odpowiedzi, błędy, saturacja) wchodzą do codziennego zestawu sygnałów SRE/DevOps. Przy każdej zmianie biznesowej, która dotyka istniejącej funkcjonalności, najpierw aktualizuje się projekt kontraktu (OpenAPI, schematy zdarzeń), a dopiero potem implementację.

Takie podejście umożliwia implementację praktyk „API first” i znacząco ogranicza ryzyko rozjazdu między oczekiwaniami biznesu a tym, co faktycznie jest dostępne dla klientów systemu.

Dobre granice mikroserwisów a kształt API

Domena biznesowa i bounded context jako podział API

Najsolidniejszą podstawą dla podziału na mikroserwisy i ich API jest domena biznesowa, mapowana za pomocą DDD (Domain-Driven Design). Każdy mikroserwis powinien reprezentować bounded context – spójny fragment rzeczywistości biznesowej ze swoim modelem danych i własnym językiem pojęć. Granice mikroserwisu to granice rozsądku dla API.

Jeżeli zespół zaczyna od bazy danych, a nie od domeny, typowy efekt to API reprezentujące tabele zamiast pojęć biznesowych. Kontrakt pełen nazw typu user_profile_details, user_address_ext czy order_items_tmp jest sygnałem, że API przecieka implementacją. O wiele lepiej zdefiniować zasoby tak, jak rozumie je biznes: /customers, /orders, /payments.

Granice kontekstów są kluczowe dla tego, ile danych pojawi się w API. Serwis „Klient” może wiedzieć bardzo dużo o użytkowniku, ale publiczny kontrakt API ujawnia tylko te pola, które są potrzebne innym kontekstom – i ani jednego więcej. Pozwala to zachować luźne powiązanie i dowolność w refaktoryzacji wewnętrznego modelu.

Symptomy złych granic: chattiness i „god object API”

Jednym z pierwszych objawów złego podziału mikroserwisów i kontraktów API jest tzw. chattiness – nadmierna rozmowność systemu. Front-end lub inny serwis musi wykonać kilka–kilkanaście wywołań, aby zrealizować prostą funkcję. Zwykle wynika to z sytuacji, gdy granice serwisów „przecinają” jeden proces biznesowy na zbyt małe kawałki, a API wymusza wiele kroków.

Przeciwnym ekstremum jest „god object API” – pojedynczy mikroserwis z ogromnym, uniwersalnym kontraktem, który udaje, że wie wszystko o wszystkim. Taki serwis jest trudny do rozwijania, ma wysoką złożoność i staje się wąskim gardłem organizacyjnym, bo większość zmian wymaga dotykania właśnie jego.

Oba zjawiska często współwystępują z problemem „anemicznych” API: endpointy zwracają za mało danych lub wymagają nadmiernego kombinowania po stronie klienta, więc rosną kolejne „pomocnicze” wywołania. W skali całego systemu oznacza to zwiększone opóźnienia, wysokie koszty utrzymania i trudności w skalowaniu.

Minimalna wiedza o wnętrzu serwisu w publicznym API

Publiczne API mikroserwisu nie powinno zdradzać szczegółów jego wewnętrznej struktury. Klient nie musi wiedzieć, że dane o zamówieniu są trzymane w trzech tabelach i dwóch kolekcjach NoSQL, ani że wewnątrz działają trzy moduły: „Koszyk”, „Zamówienie” i „Dostawa”. Kontrakt ma ujawniać spójną perspektywę domenową, a nie topologię klas i tabel.

Dlatego w dobrym API rzadko widać pola typu internal_status_code czy is_recalculated_flag, które są wyłącznie techniczną optymalizacją wewnątrz serwisu. Jeżeli takie pola muszą być wystawione, powinny być dobrze udokumentowane i mieć jasne znaczenie z punktu widzenia klienta, np. recalculation_required jako sygnał, że dane są częściowo nieaktualne i zostaną odświeżone.

Takie rozdzielenie odpowiedzialności minimalizuje chattiness, ogranicza wiedzę krzyżową i pozwala każdemu serwisowi ewoluować niezależnie, co w dłuższej perspektywie utrzymuje architekturę w ryzach. Dla osób rozwijających aplikacje webowe i backend w duchu mikroserwisów pomocne bywają materiały z serwisów takich jak Porady-IT.pl – Kurs PHP, Webmastering i Skrypty dla Nowoczesnych, gdzie temat granic i ról w architekturze jest omawiany od strony praktycznej.

Ta zasada dotyczy też identyfikatorów. Stabilne ID w API nie muszą odpowiadać głównym kluczom w bazie danych. Często lepiej używać losowych UUID lub identyfikatorów specyficznych dla kontekstu niż „przeciekać” autoinkrementowane id z jednej konkretnej tabeli.

Przykład: serwis Zamówienia kontra Koszyk i Płatności

Typowy sklep internetowy dobrze ilustruje znaczenie granic mikroserwisów i ich API. Rozsądny podział to co najmniej:

  • mikroserwis Koszyk – zarządza zawartością koszyka użytkownika przed złożeniem zamówienia,
  • mikroserwis Zamówienia – powstaje w momencie potwierdzenia koszyka i reprezentuje zobowiązanie sklepu wobec klienta,
  • mikroserwis Płatności – odpowiada za autoryzację i rozliczenie płatności.

API serwisu „Koszyk” kończy się w chwili, gdy użytkownik klika „Zamów”. Jego odpowiedzialność to umożliwienie modyfikacji pozycji, wyliczanie sum częściowych, promocji itp. Mikroserwis „Zamówienia” przejmuje stery dopiero od momentu utworzenia zamówienia – jego API nie służy do edycji koszyka, a do śledzenia statusu zamówienia, adresu dostawy, historii zmian.

Serwis „Płatności” z kolei nie musi znać szczegółów produktów w zamówieniu, wystarczą mu informacje o kwocie, walucie, identyfikatorze zamówienia i metodzie płatności. W dobrze zaprojektowanym API dane te są przekazywane w sposób spójny, bez powielania logiki domenowej „Zamówień” w kontrakcie „Płatności”.

Projektowanie kontraktu API – zasady ogólne

Modelowanie zasobów i operacji: REST/HTTP, RPC, GraphQL

Większość nowoczesnych aplikacji webowych opiera się na HTTP/REST jako podstawowym stylu API. Dobrze zaprojektowane REST API to takie, w którym zasoby i operacje są jasno rozdzielone: zasoby reprezentują „rzeczy” (np. /orders, /customers), a operacje są mapowane na metody HTTP (GET, POST, PUT, DELETE, PATCH).

REST sprawdza się tam, gdzie dominują operacje CRUD (create, read, update, delete) i gdzie aplikacja webowa pobiera względnie oczywiste widoki danych. Jednak przy bardziej proceduralnych scenariuszach (np. „przelicz wszystkie prowizje za dany okres” lub „zainicjuj kampanię marketingową”) wygodniejsze bywa RPC (Remote Procedure Call). Wtedy API opisuje raczej operacje (komendy) niż zasoby.

GraphQL jest szczególnie przydatny, gdy front-end potrzebuje składać w jednym zapytaniu dane z wielu domen (np. profil klienta + ostatnie zamówienia + status subskrypcji). Zamiast serii wywołań REST możliwe jest zdefiniowanie jednego zapytania GraphQL. Ceną jest jednak większa złożoność warstwy BFF (Backend For Frontend) i konieczność bardzo świadomego mapowania na mikroserwisy w tle.

Spójne nazewnictwo i konwencje

Spójność nazewnictwa w API ma ogromny wpływ na ergonomię pracy z systemem. Zasoby powinny być nazwane w liczbie mnogiej (np. /orders, /products), a identyfikatory przekazywane jako segment ścieżki: /orders/{orderId}. Wyjątkiem mogą być „kolekcje logiczne” jak /health, ale im mniej odstępstw, tym lepiej.

Pola w JSON powinny mieć jednolitą konwencję w całym ekosystemie: albo snake_case, albo camelCase, ale bez mieszania. Zespół powinien podjąć decyzję raz, najlepiej na etapie pierwszych mikroserwisów, i trzymać się jej niezależnie od języka programowania użytego wewnątrz serwisu.

Warto też ujednolicić:

  • nazwy pól identyfikatorów: np. id lub {resource}Id,
  • standardy paginacji: ?page= i ?page_size= albo ?limit=/?offset=,
  • konwencje filtrów: np. ?status=ACTIVE, ?created_from=, ?sort=-created_at.

Konsekwencja w tych detalach sprawia, że nowe API czuje się „jak stare”, a programiści front-endu nie muszą czytać całej dokumentacji, żeby domyślić się podstawowych parametrów.

Czytelne kody HTTP, struktura odpowiedzi i błędów

Spójna semantyka kodów i ciała odpowiedzi

Dobre API mikroserwisowe używa kodów HTTP w sposób semantyczny, a nie „byle działało”. Po stronie klienta upraszcza to obsługę błędów, retry i monitoring. Podstawowy zestaw, który zazwyczaj wystarcza:

  • 200 OK – poprawne odczyty zasobów (GET),
  • 201 Created – poprawne utworzenie zasobu (POST), z nagłówkiem Location wskazującym URL nowego zasobu,
  • 202 Accepted – operacje asynchroniczne, gdy przyjęto żądanie, ale przetwarzanie trwa dalej (np. generowanie raportu),
  • 204 No Content – poprawne wykonanie bez treści odpowiedzi (np. DELETE),
  • 400 Bad Request – walidacja wejścia, niepoprawne parametry,
  • 401 Unauthorized / 403 Forbidden – problemy z autoryzacją/autentykacją,
  • 404 Not Found – zasób nie istnieje lub jest niedostępny w danym kontekście,
  • 409 Conflict – konflikt wersji, naruszenie unikalności,
  • 422 Unprocessable Entity – domenowe błędy biznesowe (np. „nie można anulować już zrealizowanego zamówienia”),
  • 429 Too Many Requests – throttling / rate limiting,
  • 5xx – problemy po stronie serwera; 500 dla niesklasyfikowanych, 503 dla niedostępności zależności itp.

Struktura odpowiedzi – zarówno sukcesów, jak i błędów – powinna być przewidywalna. Typowy, prosty wzorzec dla sukcesu:

{
  "data": { ... },
  "meta": { ... }
}

W przypadku list przydaje się sekcja meta z paginacją:

{
  "data": [ ... ],
  "meta": {
    "total": 123,
    "page": 2,
    "page_size": 20
  }
}

Standardowy format błędów: kod, typ, szczegóły

Brak spójnego modelu błędów pomiędzy mikroserwisami kończy się if-ologią po stronie klienta. Lepszy jest uzgodniony kontrakt, np. inspirowany RFC 7807 (Problem Details for HTTP APIs):

{
  "type": "https://errors.example.com/orders/order_not_cancellable",
  "title": "Order cannot be cancelled",
  "status": 422,
  "code": "ORDER_NOT_CANCELLABLE",
  "detail": "Order already shipped.",
  "instance": "/orders/123",
  "errors": [
    {
      "field": "status",
      "message": "Order already shipped."
    }
  ]
}

Przy niewielkim wysiłku da się:

  • odróżnić błędy techniczne od biznesowych (np. po prefiksie TECH_ / BUS_ w polu code),
  • ułatwić mapowanie błędów na komunikaty UI,
  • agregować statystyki błędów per code w narzędziach typu Prometheus/Grafana.

Uwaga: komunikaty tekstowe (detail) nie powinny zdradzać szczegółów technicznych ani danych wrażliwych. Logi wewnętrzne mogą być bardziej „szczere”, publiczny kontrakt – nie.

Konsekwentne użycie nagłówków i cache

HTTP oferuje dużo więcej niż „GET z JSON-em”. W architekturze mikroserwisowej dobrze wykorzystane nagłówki zmniejszają obciążenie i poprawiają przewidywalność:

  • ETag i If-None-Match – umożliwiają cache po stronie klienta i proxy oraz wspierają kontrolę wersji zasobu (optimistic locking),
  • Cache-Control – zarządza tym, jak długo odpowiedź może być cache’owana przez przeglądarki i CDN,
  • Correlation-Id (lub Trace-Id) – identyfikator żądania propagowany między serwisami, niezbędny do śledzenia przepływów,
  • Retry-After – przydaje się z kodem 429 lub 503, sygnalizując klientowi, kiedy spróbować ponownie.

W systemach o wysokim ruchu sensowne ustawienie HTTP cache na warstwie BFF/API Gateway potrafi zdjąć znaczącą część ruchu z mikroserwisów backendowych.

Jeśli interesują Cię konkrety i przykłady, rzuć okiem na: Role i obowiązki w zespole projektującym mikroserwisy.

Młody mężczyzna planuje strategię na tablicy w biurze
Źródło: Pexels | Autor: Startup Stock Photos

REST, RPC, GraphQL, eventy – dobór stylu API do problemu

Kiedy REST faktycznie działa najlepiej

REST/HTTP jest najbardziej naturalny tam, gdzie:

  • model domeny da się sensownie opisać jako kolekcje zasobów,
  • frontend (lub inny klient) głównie odczytuje dane lub wykonuje proste mutacje,
  • łatwo zdefiniować stabilne URI dla kluczowych bytów biznesowych.

Przykłady: katalog produktów, profile użytkowników, historia zamówień, konfiguracje aplikacji. W takich obszarach „czyste” REST-owe CRUD + parę komend (np. POST /orders/{id}/cancel) są prostsze niż budowanie osobnego RPC.

RPC jako kontrakt komend domenowych

RPC (np. JSON-RPC górą HTTP, gRPC, wewnętrzne API oparte na komendach) dobrze pasuje do przypadków, gdy:

  • dominuje logika proceduralna („zrób X, potem Y”),
  • operacja nie ma oczywistej reprezentacji jako zasób,
  • ważniejsza jest akcja niż stan.

Dobry przykład: serwis rozliczeń, który cyklicznie generuje zestawienia prowizji, czy serwis odpowiedzialny za migrację danych między systemami. Endpoint typu:

POST /commission/calculate
Content-Type: application/json

{
  "period_from": "2024-01-01",
  "period_to": "2024-01-31",
  "sales_channel": "ONLINE"
}

jest zrozumiały jako „procedura”, nie jako tworzenie zasobu. W mikroserwisach wewnętrzne kontrakty RPC często implementuje się gRPC ze względu na wydajność, typowanie i generowanie klientów.

GraphQL jako „agregator” danych, nie bezpośredni mikroserwis

GraphQL świeci tam, gdzie frontend potrzebuje komponować dane z wielu źródeł i ma dużą zmienność widoków (np. rozbudowane SPA, panele administracyjne). Kluczowe zasady przy łączeniu GraphQL i mikroserwisów:

  • GraphQL najczęściej powinien żyć na warstwie BFF/API Gateway jako fasada nad mikroserwisami,
  • schemat GraphQL nie musi 1:1 odzwierciedlać modeli domenowych poszczególnych serwisów – ma odpowiadać potrzebom UI,
  • resolver dla pojedynczego pola w GraphQL nie może wywoływać „po trochu” dziesięciu mikroserwisów – inaczej uzyskujemy N+1 problem na poziomie sieci.

Praktyka: serwis „Profile użytkownika” może oferować REST-owe /customers, /subscriptions, a GraphQL-owy BFF łączy to w typ:

type CustomerProfile {
  customer: Customer!
  recentOrders: [Order!]!
  activeSubscriptions: [Subscription!]!
}

Front-end ma elastyczność, a mikroserwisy pozostają relatywnie proste i niezależne.

API zdarzeniowe (eventy) jako fundament luźnego powiązania

API nie musi oznaczać wyłącznie HTTP. W mikroserwisach krytyczną rolę pełnią zdarzenia domenowe (eventy), publikowane na brokerze (np. Kafka, RabbitMQ, NATS). Zdarzenie typu:

{
  "eventType": "ORDER_PLACED",
  "eventId": "uuid",
  "occurredAt": "2024-05-01T10:15:23Z",
  "data": {
    "orderId": "123",
    "customerId": "456",
    "totalAmount": 199.99,
    "currency": "PLN"
  }
}

staje się publicznym kontraktem tak samo jak REST-owe /orders. Serwisy „Płatności”, „Lojalność” czy „Powiadomienia” mogą się podpiąć bez zmiany serwisu „Zamówienia”.

Tip: eventy powinny być faktami, które się wydarzyły (np. PAYMENT_AUTHORIZED), a nie poleceniami („spróbuj pobrać płatność”). Komendy to osobny kanał (np. HTTP, kolejka komend), eventy – to informacja, że coś już zaszło.

Wersjonowanie i ewolucja API bez zatrzymywania produkcji

Wersjonowanie kontraktu: ścieżka, nagłówek czy schemat?

W mikroserwisach wersjonowanie jest nieuniknione. Najpopularniejsze podejścia do REST/HTTP:

  • wersja w ścieżce, np. /v1/orders, /v2/orders – proste routowanie, ale długoterminowo generuje „martwe” wersje,
  • wersja w nagłówku, np. Accept: application/vnd.shop.orders.v2+json – czyste ścieżki, lecz trudniejsze do debugowania „gołym okiem”,
  • wersja w parametrze, np. /orders?api-version=2 – mało eleganckie, ale łatwe do wprowadzenia bez zmiany DNS/routingu.

W praktyce kluczowe nie jest miejsce wersji, tylko dyscyplina: jakie zmiany wymagają nowej wersji, a jakie można rozwiązać kompatybilnie wstecz.

Zmiany kompatybilne wstecz vs. łamiące kontrakt

Zmiany kompatybilne wstecz (backward compatible), które zazwyczaj nie wymagają nowej wersji:

  • dopisanie nowego pola w odpowiedzi (klienci powinni ignorować nieznane pola),
  • dopuszczenie nowych wartości wyliczenia (enum), jeśli klienci obsługują „domyślną” ścieżkę dla nieznanych wartości,
  • dopisanie nowych endpointów, które nie zmieniają zachowania istniejących.

Zmiany łamiące kontrakt (wymagają nowej wersji lub migracji wieloetapowej):

  • usunięcie pola lub zmiana jego typu (np. z liczby na string),
  • zmiana semantyki istniejącego pola (np. status=ACTIVE zaczyna oznaczać coś innego),
  • zmiana formatów dat, walut, kluczy głównych.

Bezpieczna zasada: nie ruszać tego, co już zostało opublikowane; nowe zachowania dorzucać równolegle (np. nowe pole, nowy endpoint, nowy typ eventu).

Strategia „expand and contract” (rozszerz i skurcz)

Klasyczna technika ewolucji kontraktu bez downtime:

  1. Expand – serwis zaczyna obsługiwać „stary” i „nowy” kształt danych równocześnie (np. przyjmuje zarówno phone, jak i phone_numbers[]),
  2. klienci są stopniowo migrowani na nowe pola/endpointy,
  3. monitoring śledzi, czy ktoś wciąż używa starego kontraktu,
  4. Contract – po okresie przejściowym stary kontrakt oznaczany jest jako deprecated, a następnie usuwany.

W architekturze mikroserwisów, gdzie klientami są często inne mikroserwisy, proces ten trzeba traktować jak projekt: mieć plan migracji, właścicieli, terminy, alarmy.

Ewolucja eventów: dodawanie pól, nowe typy zdarzeń

Kontrakty eventowe także wymagają wersjonowania. Kilka prostych zasad trzyma system w ryzach:

  • dodawanie pól do data jest zwykle bezpieczne, o ile konsumenci ignorują nieznane pola,
  • nie usuwać i nie zmieniać znaczenia istniejących pól – lepiej dodać nowe pole i oznaczyć stare jako deprecated w dokumentacji,
  • poważniejsze zmiany modelu wyrażać poprzez nowy eventType, np. ORDER_PLACED_V2, zamiast wersjonowania całego strumienia,
  • każde zdarzenie powinno mieć schemaVersion lub podobne pole, by konsument mógł zdecydować, czy je poprawnie rozumie.

Wzorce komunikacji między mikroserwisami (synchronicznie i asynchronicznie)

Komunikacja synchroniczna: request/response z ograniczeniami

Synchroniczne wywołania HTTP/gRPC kuszą prostotą: „potrzebuję danych – wywołam serwis X”. Problem pojawia się, gdy:

  • łańcuch żądań jest długi (A woła B, B woła C…),
  • brak limitów czasowych (timeoutów) i retry,
  • brak odporności na częściowe awarie.

Efekt to „kaskady błędów”: jeden wolny serwis przyblokuje pół systemu. Dlatego w kontrakcie synchronicznym trzeba od razu przewidzieć:

  • twarde timeouty po stronie klienta i serwera,
  • jasne kody i formaty błędów pozwalające odróżnić sytuacje do retry od tych, które wymagają interwencji biznesowej,
  • ograniczenie „głębokości” łańcucha wywołań – kluczowe procesy powinny opierać się na eventach, nie na kaskadach REST.

Komunikacja asynchroniczna: message broker jako kręgosłup

Wzorce integracji asynchronicznej w praktyce

Sam wybór brokera (Kafka, RabbitMQ, NATS, SNS/SQS) to dopiero początek. O jakości API zdarzeniowego decyduje, jak modelujesz komunikację między serwisami. Kilka sprawdzonych schematów:

  • publish/subscribe (pub/sub) – serwis publikuje zdarzenia domenowe, wiele innych serwisów może je konsumować niezależnie; dobry fundament „luźnego sprzężenia”,
  • kolejki robocze (work queues) – zadania są rozkładane między wielu konsumentów (workerów), zwykle bez potrzeby zachowania pełnej historii,
  • event streaming – zdarzenia są przechowywane przez dłuższy czas, konsumenci mogą je czytać od wybranego offsetu (np. ponowna analiza historii zamówień).

W projektowaniu API zdarzeniowego najważniejsza jest spójność kontraktu: ten sam typ zdarzenia zawsze ma ten sam sens biznesowy i strukturę (poza ewolucją kontrolowaną przez wersje).

Event-driven vs. choreografia i orkiestracja

Procesy biznesowe w mikroserwisach można spinać dwiema głównymi technikami:

Do kompletu polecam jeszcze: Skalowanie mikroserwisów poziome vs pionowe — znajdziesz tam dodatkowe wskazówki.

  • choreografia – brak centralnego koordynatora; serwisy reagują na eventy i emitują kolejne, np. ORDER_PLACED → „Płatności” emitują PAYMENT_AUTHORIZED → „Wysyłka” reaguje i przygotowuje paczkę,
  • orkiestracja – jeden serwis (lub dedykowany orkiestrator / silnik workflow) steruje procesem, wywołując inne serwisy synchronicznie lub asynchronicznie.

Choreografia jest prostsza na starcie i dobrze skaluje się organizacyjnie, ale łatwo prowadzi do „tańca, którego nikt nie rozumie”, jeśli nie ma dobrej dokumentacji eventów. Orkiestracja daje jedno miejsce prawdy o scenariuszu, kosztem silniejszego powiązania z orkiestratorem.

API mikroserwisów nie powinno przeciekać szczegółami konkretnego sposobu integracji. Z punktu widzenia serwisu zdarzenia nadal są „tylko” publicznym kontraktem: dane wejściowe i wyjściowe procesu, niezależnie od tego, czy ktoś je potem układa w choreografię, czy orkiestruje.

Integracja synchroniczno-asynchroniczna (hybrid)

W rzeczywistych systemach rzadko da się wybrać wyłącznie synchroniczny albo asynchroniczny styl. Typowy schemat to hybryda:

  • komunikacja do UI i BFF – głównie synchroniczna (HTTP/GraphQL),
  • wewnątrz domeny – eventy domenowe, czasem RPC/gRPC,
  • zadania długotrwałe – asynchroniczne kolejki, z callbackiem/zdarzeniem na zakończenie.

Dobry kontrakt API powinien jasno sygnalizować, co jest „gorącą ścieżką” synchroniczną (np. potwierdzenie koszyka), a co ma charakter „fire-and-forget” lub „zobaczysz efekt za chwilę” (np. rekalkulacja rekomendacji produktowych).

Stabilność, idempotencja i odporność API w świecie mikroserwisów

Idempotencja operacji – nie tylko dla HTTP PUT

Idempotencja oznacza, że wielokrotne wywołanie tej samej operacji z tymi samymi danymi prowadzi do tego samego efektu końcowego. W mikroserwisach to broń przeciwko błędom sieci, retry i duplikatom eventów.

W praktyce idempotencję można zaimplementować na kilka sposobów:

  • idempotency key – klient generuje unikalny identyfikator żądania (np. Idempotency-Key: uuid), serwis zapisuje efekt pierwszego przetworzenia i dla kolejnych żądań z tym samym kluczem zwraca taki sam rezultat,
  • naturalny klucz biznesowy – np. PUT /customers/{customerId} z pełnym stanem klienta; wielokrotne wywołanie z tym samym body ustala ten sam stan,
  • deduplikacja eventów – każde zdarzenie ma unikalne eventId; konsument utrzymuje listę (lub skrót) obsłużonych ID i ignoruje duplikaty.

Uwaga: idempotencja dotyczy efektu biznesowego, nie tego, że logi czy metryki nie urosną dwukrotnie. Wielokrotne wykonanie będzie widoczne w obserwowalności, natomiast nie powinno zmienić stanu domeny.

Kontrakt błędów i semantyka odpowiedzi

Wzorzec „zwracaj cokolwiek, byle 200 OK” utrudnia budowanie odpornych klientów. Przemyślany kontrakt błędów to fundament stabilności API. Dobrą praktyką jest spójny format odpowiedzi o błędach, np.:

{
  "errorCode": "PAYMENT_LIMIT_EXCEEDED",
  "message": "Daily payment limit has been exceeded.",
  "details": {
    "limit": 5000,
    "currency": "PLN"
  },
  "correlationId": "d1c2..."
}

Kilka zasad, które ułatwiają życie integratorom:

  • rozróżniaj błędy klienckie (4xx – brak uprawnień, walidacja, konflikt) od serwerowych (5xx – problemy techniczne),
  • podawaj correlationId lub traceId w każdej odpowiedzi, by można było prześledzić przepływ żądania przez mikroserwisy,
  • utrzymuj stabilną listę errorCode jako część kontraktu API – zmiana kodu bez migracji klientów jest tak samo bolesna jak zmiana pola w JSON.

Time-outy, retry i circuit breaker jako część API

Mechanizmy odpornościowe często traktuje się jako sprawę „frameworka” albo warstwy sieci. W mikroserwisach stają się jednak częścią kontraktu: klient musi wiedzieć, jak i kiedy ponawiać wywołania.

  • Timeouty – dokumentuj oczekiwane czasy odpowiedzi; endpoint, który „w normalu” odpowiada 5 sekund, będzie zabójczy dla łańcucha zależności.
  • Retry – określ, które błędy są bezpieczne do ponawiania (zwykle 5xx, czasem 409/429) i po stronie serwisu zapewnij idempotencję tych operacji.
  • Circuit breaker – przy długotrwałych awariach lepiej jest szybko zwrócić kontrolowany błąd (np. „serwis płatności niedostępny”), niż trzymać klientów w limbo przez 60 sekund.

Tip: przy asynchronicznych eventach „retry” często jest wbudowane w brokera (redelivery). Wtedy odpowiednikiem circuit breakera staje się decyzja: po ilu nieudanych próbach trafiamy do dead-letter queue i uruchamiamy proces ręcznego wyjaśniania.

Wzorce odporności: bulkhead, fallback i cache

Gdy pojedynczy serwis zależy od wielu innych, jeden słaby punkt może go pociągnąć na dno. Kilka wzorców pomaga to ogarnąć:

  • bulkhead (przegrody) – osobne pule połączeń i limity dla różnych zależności; awaria katalogu rekomendacji nie powinna wykorzystać wszystkich wątków serwisu koszyka,
  • fallback – sensowne wartości domyślne lub degradacja funkcjonalności, np. „brak rekomendacji, ale nadal można kupić produkt”,
  • cache wyników – dla danych rzadko zmieniających się (cenniki, konfiguracja) cache po stronie klienta zmniejsza zależność od dostępności serwisu źródłowego.

Te mechanizmy są techniczne, ale wpływają na kształt API: musisz zdefiniować, co znaczy „tryb zdegradowany” i jaki format odpowiedzi dostaje klient, gdy część danych pochodzi z cache lub fallbacku.

Transakcje rozproszone i spójność ostateczna

Klasyczne transakcje ACID na poziomie bazy nie przechodzą testu mikroserwisów. Trzeba świadomie projektować API tak, aby akceptowało spójność ostateczną (eventual consistency). Przykładowo:

  • po złożeniu zamówienia status płatności może przez pewien czas być „PENDING”,
  • panel admina może pokazywać dane z opóźnieniem kilkusekundowym względem systemu transakcyjnego.

Zamiast globalnych transakcji używa się wzorca Saga: sekwencja lokalnych transakcji, powiązanych zdarzeniami i ewentualnymi akcjami kompensacyjnymi. To przekłada się na API w kilku miejscach:

  • statusy procesów zawierają stany pośrednie (PENDING, IN_PROGRESS, FAILED, COMPENSATED),
  • pojawiają się endpointy lub eventy kompensacyjne, np. ORDER_CANCELLED, które odkręcają wcześniejsze kroki,
  • kontrakty nie obiecują natychmiastowej spójności; zamiast tego jasno mówią „operacja przyjęta, efekt końcowy będzie widoczny później”.

Obserwowalność jako część kontraktu API

Bez wbudowanej obserwowalności debugowanie problemów między mikroserwisami zamienia się w zgadywankę. Dobre API eksponuje nie tylko dane biznesowe, ale i kontekst techniczny:

  • nagłówki korelacyjne (X-Correlation-Id, Traceparent) są przekazywane przez wszystkie warstwy,
  • standardowy schemat logowania żądań i odpowiedzi (bez wrażliwych danych) pozwala odtworzyć ścieżkę wywołań,
  • metryki (czas odpowiedzi, liczba błędów, rozkład kodów HTTP) powiązane są z konkretnymi endpointami i typami eventów.

Część organizacji idzie krok dalej i traktuje budowę metryk i logów jako element definicji „gotowości” API: nie można wystawić nowego endpointu, jeśli nie ma on sensownej telemetrii.

Bezpieczeństwo i limity jako „twardy” aspekt stabilności

Ochrona API to nie tylko autoryzacja i JWT. Dla stabilności całego systemu równie ważne są:

  • rate limiting – ograniczenia liczby żądań na klienta / token / IP; najlepiej centralnie (API Gateway) i lokalnie (po stronie serwisu),
  • quota – limity „miękkie” lub „twarde” dla zasobów, np. liczba aktywnych subskrypcji, co przeciwdziała atakom logiki biznesowej,
  • throttling – kontrolowane spowalnianie nadmiarowego ruchu zamiast brutalnego odcinania wszystkich.

Kontrakt API powinien jasno definiować, jakie kody i struktury odpowiedzi pojawiają się przy przekroczeniu limitów (np. 429 z informacją o czasie resetu), żeby klienci mogli na nie reagować w przewidywalny sposób.

Projektowanie pod „failure modes” zamiast „happy path only”

Ostatni krok do naprawdę odpornych API w mikroserwisach to wyjście poza „scenariusz idealny”. Warto jawnie zmapować typowe tryby awarii:

  • częściowa niedostępność (jeden z zależnych serwisów leży),
  • degradacja wydajności (opóźnienia rosną, ale serwis odpowiada),
  • nieoczekiwane dane (nowe wartości enum, duże payloady, brakujące pola).

Projekt kontraktu powinien uwzględniać, co się wtedy dzieje: jakie statusy zwracasz, jakie komunikaty emisujesz do użytkownika, kiedy automatycznie przerywasz próbę i odsyłasz klienta do mechanizmu „spróbuj później”. To nie są „edge case’y” – w systemach rozproszonych to codzienność, którą trzeba zaprojektować świadomie w API.

Najczęściej zadawane pytania (FAQ)

Co to znaczy, że API w mikroserwisach jest „kontraktem”, a nie tylko transportem danych?

API jako kontrakt oznacza, że opisuje ono jasno: jakie zasoby są dostępne, jakie operacje można wykonać, jakie dane są zwracane i jakie błędy mogą się pojawić. To zestaw reguł uzgodniony między dostawcą a konsumentem, a nie tylko „rura HTTP”, którą lecą dowolne JSON-y.

Taki kontrakt jest podstawą niezależności zespołów: backend, frontend, QA i DevOpsy mogą pracować równolegle, o ile trzymają się ustalonej specyfikacji (np. OpenAPI). Zmiana kontraktu jest świadomą decyzją biznesowo‑techniczną, a nie „przy okazji refaktoryzacji”.

Jakie są najczęstsze skutki źle zaprojektowanego API w architekturze mikroserwisów?

Typowy efekt to silne sprzężenie między serwisami: drobna zmiana w jednym API wymaga zmian w kilku innych, pojawiają się skoordynowane releasy i „okna serwisowe”. Mikroserwisy zaczynają zachowywać się jak rozbity monolit, z którym trudno pracować i który trudno wdrażać częściej niż raz na jakiś czas.

Drugi problem to kruchość całego systemu. Zbyt długi łańcuch synchronicznych wywołań, brak time‑outów, brak wzorców ochronnych (circuit breaker, retry z backoffem) prowadzą do efektu domina: lawiny retry, zablokowanych wątków i utraty dostępności. Często wychodzi to na jaw dopiero pod obciążeniem produkcyjnym.

Czym różni się API monolitu od API w architekturze mikroserwisów?

W monolicie API to zazwyczaj jedna, spójna warstwa nad wspólnym procesem. Moduły wewnątrz mogą być mocno powiązane, bo i tak wdrażasz wszystko naraz i możesz „naprawić” obu konsumentów jedną kompilacją. Opóźnienia sieci, częściowe awarie czy wersjonowanie między modułami praktycznie nie istnieją.

W mikroserwisach każdy serwis ma własne API, cykl życia, release i wersje. Kontrakty między serwisami trzeba traktować jak zewnętrzne: dbać o kompatybilność wsteczną, odporność na opóźnienia i niedostępność partnera. Zespół nie ma możliwości wymusić jednoczesnej aktualizacji wszystkich klientów w jednym sprincie.

Jak dobrze wyznaczyć granice mikroserwisów i ich API w oparciu o domenę biznesową?

Najbezpieczniej oprzeć się na DDD i bounded contextach: jeden mikroserwis odpowiada za spójny fragment domeny (np. „Zamówienia”, „Płatności”, „Klienci”) i ma własny język pojęć oraz model danych. API tego serwisu wystawia pojęcia biznesowe, a nie tabele z bazy.

Dobrym testem jest nazewnictwo. Endpointy typu /customers, /orders, /payments zwykle oznaczają modelowanie domenowe. Natomiast kontrakt pełen user_profile_details, order_items_tmp czy address_ext sugeruje, że API przecieka wewnętrzną strukturą tabel. Uwaga: granice serwisów wpływają bezpośrednio na to, ile danych i jakiej jakości trafia do API.

Jak rozpoznawać i unikać „chattiness” oraz „god object API” w mikroserwisach?

Chattiness to sytuacja, w której klient (frontend lub inny serwis) musi wykonać wiele wywołań, aby zrealizować prosty use case. Przykład: ekran „szczegóły zamówienia” wymaga osobnych calli do serwisów: zamówień, produktów, płatności i wysyłki, zamiast jednego dobrze zaprojektowanego endpointu z widokiem agregującym.

„God object API” to przeciwległy biegun: jeden mikroserwis próbuje wiedzieć wszystko o wszystkim i ma ogromne, uniwersalne API. Oba przypadki są sygnałem, że granice domenowe i kontrakty wymagają przemyślenia. Pomaga: wprowadzenie dedykowanych widoków (np. API do „read modelu” pod konkretne ekrany) oraz refaktoryzacja domeny na kilka spójnych, mniejszych kontekstów.

Dlaczego publiczne API nie powinno ujawniać szczegółów wewnętrznej implementacji mikroserwisu?

Jeśli publiczne API zdradza szczegóły wnętrza serwisu (np. nazwy tabel, statusy techniczne, flagi optymalizacyjne), to każdy wewnętrzny refaktor zaczyna być zmianą kontraktu. Konsumenci stają się zależni od szczegółów, które w ogóle nie powinny ich interesować. To szybko blokuje ewolucję serwisu.

Lepsze podejście: API opisuje stabilne pojęcia domenowe i tylko te pola, które są rzeczywiście potrzebne klientom. Techniczne detale, takie jak internal_status_code albo is_recalculated_flag, zostają wewnątrz. Jeśli jakieś pole techniczne musi trafić do kontraktu, powinno mieć jasne znaczenie biznesowe i dobrą dokumentację, np. recalculation_required jako sygnał, że klient musi odświeżyć dane.

Jak praktycznie stosować podejście „API first” w cyklu życia mikroserwisów?

API first zaczyna się od analizy biznesowej: zdarzenia domenowe, przypadki użycia, bounded contexty. Na tej bazie tworzy się kontrakty (OpenAPI, schematy zdarzeń), które stają się „źródłem prawdy” dla wszystkich zespołów. Implementacja backendu i frontendu jest wtórna wobec tego kontraktu, a nie odwrotnie.

W praktyce oznacza to m.in.: testy kontraktowe między zespołami, generowanie klientów ze specyfikacji, automatyczne sprawdzanie kompatybilności wstecznej oraz monitorowanie metryk API (czas odpowiedzi, błędy, saturacja). Tip: każdą zmianę biznesową zaczynaj od aktualizacji kontraktu, a dopiero potem dotykaj kodu serwisu.

Najważniejsze punkty

  • API w mikroserwisach jest kontraktem, a nie „rurą HTTP” – opisuje zasoby, operacje, format danych i błędy, dzięki czemu zespoły mogą rozwijać system niezależnie i bez ciągłej koordynacji.
  • Kontrakt API musi być wspólnym punktem odniesienia dla backendu, front-endu, testerów, DevOpsów i analityków; specyfikacja OpenAPI pełni rolę „źródła prawdy” oraz służy do generowania klientów, stubów i testów kontraktowych.
  • Zły projekt API prowadzi do ciasnego sprzężenia mikroserwisów, trudnych wdrożeń i efektu domina przy awariach (łańcuch synchronicznych wywołań, brak idempotentnych endpointów, brak circuit breakerów i sensownych time-outów).
  • W architekturze mikroserwisów każde API należy traktować jak zewnętrzne: zapewniać kompatybilność wsteczną, brać pod uwagę opóźnienia sieci i częściowe awarie oraz stosować świadome wersjonowanie, zwłaszcza dla API publicznych.
  • Rozróżnienie API zewnętrznego i wewnętrznego pozwala inaczej ustawić wymagania: API publiczne musi być stabilne i ostrożnie ewoluować, podczas gdy API wewnętrzne może zmieniać się szybciej przy lepszej koordynacji zespołów.
  • API powinno być projektowane „API first” jako część pełnego cyklu życia funkcjonalności: od analizy domeny, przez kontrakt (OpenAPI, schematy zdarzeń), testy kontraktowe, aż po monitoring metryk (czas odpowiedzi, błędy, saturacja).