Nie sposób napisać samowystarczalny projekt, potrzebne jest wykorzystanie dodatkowych usług, dzięki którym jego funkcjonalność wzrośnie. Dostawcy metod płatności, marketingowe kanały promocji produktów, porównywarki cenowe, integracja z dodatkowymi systemami sprzedaży, wysyłki kurierskie, zaawansowane analizy danych. Korzystanie z nich wymaga komunikacji z API, dostarczania i odbierania danych, reagowania na zmiany i zdarzające się awarie.

Im większa liczba usług, tym trudniej jest nad nimi zapanować, stąd też kluczowe jest to, aby architektura projektu oferowała sposoby i narzędzia do ich łatwiejszego implementowania, konfigurowania i wyłączania. Możliwości te oferuje na przykład architektura headless i w tym artykule przyjrzymy się dokładniej temu, jak wygląda w niej implementacja usług.

Usługi zewnętrzne w projekcie

Różne są typy usług. Czasem można posiadać kilka realizujących tę samą krytyczną funkcjonalność, aby w razie problemów przełączyć się na zapasową. Są też takie, które oferują nam dodatkowe możliwości, np. analityczne lub monitorujące i w przypadku pojedynczych problemów w ich funkcjonowaniu, nie musimy przerywać trwającego procesu, np. składania zamówienia. W zależności od usługi różne są zatem oczekiwania wydajnościowe, zwłoki w komunikacji, pokrycia danych i dopuszczalnego progu błędów. Pod kątem tych kryteriów należy ustalić, jaka powinna być czasowa dostępność usługi, jak reagować na jej awarię i jak umieścić ją w procesie, aby niepotrzebnie nie tracić na wydajności lub wydłużeniu się czasu odpowiedzi.

Implementacja usług w architekturze headlesowej

W projekcie opartym o headlesową architekturę istotny jest podział na warstwy oraz ich separację i z tej struktury należy skorzystać podczas implementacji usług. Jest to ograniczenie, którego musimy się trzymać i pod jego kątem planować architekturę tworzonego kodu. Nierzadko powoduje to powstanie dodatkowej objętości w kodzie, aby móc utrzymać wzorce i korzyści headlesowej architektury, a przy rosnącej objętości istotne jest pełne zrozumienie implementowanej usługi, aby abstrakcyjna złożoność nie poszła w parze z brakiem czytelności.

Podczas implementacji usługi zewnętrznej, istotne jest zapoznanie się z jej sposobem funkcjonowania, możliwościami dostarczanego API i pułapkami bezpieczeństwa. Te rzeczy są istotne niezależnie od tego, gdzie się je implementuje, jednakże ich znaczenie rośnie w architekturach warstwowych, na przykład w architekturze headlesowej. W tym przypadku cały obszar frontendowy zostawiamy niejako bez opieki i skazujemy go na wszystkie negatywne konsekwencje takie jak: niewłaściwe odwołania do backendu, obecność keszowania danych, czy też przerywania akcji poprzez timeouty. Przy wrażliwych usługach powinniśmy je eliminować poprzez napisanie dobrego backendu, który pozwoli na precyzyjne określenie kolejności zdarzeń, aktualnego stanu i wyłapywanie błędów na poziomie logiki biznesowej, aby uzyskać czytelny i prawidłowy przepływ danych pomiędzy naszym backendem a API usługi, bo to jest główny problem obsługi krytycznych usług. Konieczne jest bowiem dostosowywanie się pod narzucony sposób komunikacji i godzenia go z wytycznymi dla naszego projektu i wyprowadzenia w nim ścieżek dla negatywnych scenariuszy. W tym momencie mamy właściwie do czynienia ze wszystkimi bolączkami architektury mikroserwisów i w ten sposób powinniśmy rozumieć taką implementację - jako łączenie kilku aplikacji dla jednego celu biznesowego, a przy headlessie mamy przynajmniej trzy serwisy: front, backend i usługę.

Czy można zaimplementować wszystko?

Czy są usługi, których nie powinno się implementować? Cóż, ogółem nie jest dobrym pomysłem obsługiwanie czegoś, co posiada słabą dokumentację, zawiły taryfikator kosztów lub korzysta z niewydajnej infrastruktury sprzętowej. Te niedogodności będą miały większe znaczenie w kontekście tego, że w headlesowej architekturze utrzymuje się mniejsze zaufanie do frontendu z racji separacji warstw między sobą, a co za tym idzie, często też osób pracujących nad nimi. Warto dokładnie w tym miejscu zapoznać się z dokumentacją usługi, bo być może oferuje różne sposoby komunikacji, mniej lub bardziej oparte na frontendzie (lub nawet wcale); posiadanie bibliotek ułatwiających pisanie komunikacji z zewnętrznym API lub cenne how-to, wraz z przykładami rozwiązującymi problematyczne kwestie.

Sposoby implementacji

Usługi działają w różny sposób. Czy frontend musi się z nimi komunikować? Czy może wszystko jest po stronie backendowej, a frontend co najwyżej wskazuje, który rekord ma zostać przetworzony? Czy usługa dysponuje kanałem do wysyłania komunikatów, takich jak np. IPN (Instant Payment Notification)? Czy wymagana jest od nas weryfikacja pod kątem nadużyć? Czy płacimy tylko za wykonaną usługę per rekord danych, czy też za każde pojedyncze żądanie jej wykonania? Czy chcemy skorzystać z pełnych możliwości, czy tylko wybranych funkcjonalności?

Dzięki takim pytaniom jesteśmy w stanie odpowiednio ulokować usługę w naszym projekcie, w jego warstwach i określić, co z czym powinno się komunikować, które dane powinniśmy szczegółowo zapisywać, a przede wszystkim odpowiednio określić moment, w którym pojedyncza akcja została zakończona z sukcesem (na przykład zdarzenie kompletnego opłacenia zamówienia).

Instant Payment Notification

Jeśli usługa wykonywana jest z użyciem frontendu, nie możemy określić, czy faktycznie została wykonana, bo wszystkie takie informacje przekazuje nam niezaufany request, którego prawdziwość możemy albo samodzielnie zweryfikować poprzez backendowe zapytanie, albo też dostać w innym kanale komunikacyjnym odpowiedź w chwili, kiedy to faktycznie nastąpi - np. dostać powiadomienie, że zamówienie X zostało opłacone lub też uznane za wyłudzenie i, w konsekwencji, odrzucone. Dla takich powiadomień, dostawcy płatności często udostępniają wspomniany już wyżej IPN i oczekują od nas odbioru tych informacji. Musimy wtedy wystawić na zewnątrz część backendu, a otrzymywane tu informacje powiedzą nam, czy dana transakcja została zrealizowana i z jakim skutkiem. Zazwyczaj dostajemy zapewnienie, z jakich adresów IP następuje taka komunikacja lub też dysponujemy sygnaturą, której prawdziwość możemy potwierdzić za pomocą klucza prywatnego. Uzbrojeni w pewność, że dane przychodzą od dostawcy usługi, możemy na ich podstawie dokonywać aktualizacji lub korekty składowanych danych oraz wyzwalać odpowiednie zdarzenia. Niestety, powoduje to zwiększenie skomplikowania się struktury naszej implementacji, która posiada teraz już cztery kanały komunikacji: frontend, nasza baza danych, API usługi oraz kanał typu IPN.

Przykładowy schemat łączenia się z usługą zewnętrzną. Połączenia pomiędzy obiektami mogą się czasem różnić, w zależności od szczegółów implementacji.
Przykładowy schemat łączenia się z usługą zewnętrzną. Połączenia pomiędzy obiektami mogą się czasem różnić, w zależności od szczegółów implementacji.

W tej strukturze jedyne, czego możemy być pewni, to tego, co trzymamy w bazie danych, gdyż pozostałe trzy elementy podatne są na zawodność, nieaktualność i problemy w działaniu serwerów, na których funkcjonują. Musimy też w inny sposób obsłużyć każdą drogę komunikacji. Dla frontendu być pilnym strażnikiem i brać tylko najbardziej sensowne dane oraz pasujące do układanki biznesowej. Do API usługodawcy wysyłać aktualne dane i zapisywać jak najwięcej informacji zwrotnych, zwłaszcza z naciskiem na sygnatury czasowe. Z kolei informacje odbierane przez IPN należy traktować najbardziej priorytetowo, najbardziej im ufać i zadbać o ich umiejscowienie w bazie danych. Musimy też mieć na względzie, że na frontendzie może dziać się wiele, na przykład występująca tam duża liczba przekierowań może nierzadko prowadzić do sytuacji, kiedy to np. IPN pierwszy, przed requestem z frontendu, poinformuje nas o powodzeniu płatności.

Korzyści i problemy podczas implementowania usług w architekturze headlesowej

Może się wydawać, że implementacja usług w architekturze headlesowej nie powinna mieć wad w porównaniu do implementowania jej w tradycyjnej architekturze monolitu - mamy przecież nowoczesny plac budowy. Wybór architektury ma jednak przełożenie także na to, w jaki sposób implementujemy usługi zewnętrzne, na problemy, jakie możemy napotkać oraz na korzyści wynikłe ze stosowania takiego rozwiązania architektonicznego.

Przede wszystkim jednak, pisanie aplikacji headlesowej wymaga od nas przygotowywania uniwersalnego kodu, który będzie mógł się zmieniać i dostosowywać do różnych sytuacji. Sprawia to, że sama implementacja usługi to tylko połowa zadania, bo równie ważne jest wkomponowanie jej w architekturę.

Korzyści mogą być następujące:

  • Możemy uruchomić naszą implementację tylko dla wybranych rozwiązań frontendowych, np. tylko dla aplikacji mobilnej. Możemy także inaczej ją skonfigurować dla każdego z rozwiązań frontendowych i zmieniać je w czasie, zależnie od wymagań klienta.
  • Możemy bezpośrednio testować backend, pomijając implementację na froncie lub też testować z prowizorycznym frontendem w oczekiwaniu na projekt finalny.
  • Prace nad implementacją można podzielić na kilka zespołów: frontend, backend (a tu dalej dzielić zadaniowo: implementacja logiki biznesowej, dwa niezależne kanały komunikacji)
  • Możemy łatwiej wykonać przepięcie pomiędzy starą i nową wersją API (np. migracja z 1.x do 2.x) dzięki separacji backendu od frontendu.

Potencjalne problemy w implementacji usług w aplikacji headlesowej:

  • Potrzeba bycia otwartym przy dostępie do konkretnego zbioru danych (i zarządzającego nim modelem) na co najmniej dwa kanały komunikacji - z frontem oraz z usługą API. Są zupełnie inne w założeniach, a jednocześnie przetwarzają te same dane, co może prowokować do bezpośredniego przekazywania danych i pomijania procesu autoryzacji dla danych przychodzących.
  • Ryzyko wystąpienia na froncie procesów keszowania danych i ponownego wysyłania requestów, które mogą już nie być aktualne.
  • Frontendy mogą zmieniać się niezależnie od siebie i zacząć wykorzystywać usługę na różne sposoby, do czego możemy nie być przygotowani.
  • Aby w pełni wykorzystać zalety, potrzebne jest dobre gospodarowanie danymi, które przechowujemy i odpowiednie podejście do kwestii wersjonowania i istnienia różnych rozwiązań frontendowych.
  • W przypadku słabo udokumentowanej usługi, istnieje ryzyko błędnego jej rozumienia przez różne zespoły i próba posłużenia się nią w sposób niezgodny z przewidywaniami. W skrócie - ryzyko bałaganu jest większe.
  • Może powstać zawiły panel do konfiguracji usługi, a powstające w nim zmiany mieć negatywny skutek na akcje będące w trakcie realizacji.

Podsumowując, należy mieć na uwadze, że oprócz biznesowej korzyści uruchomienia jakiejś usługi, dostajemy korzyści płynące z wybrania headlesowej architektury, o ile tylko nie zapomnimy o ich obsłudze, a także o możliwości konserwacji usługi i ulepszania jej w późniejszym czasie.

Czy zatem w monolicie łatwiej integrować jest usługi?

Można nabrać wrażenia, że za dużo poświęcamy uwagi na dopasowanie się do wzorców architektury, jednakże to, że coś jest łatwiejsze do zrobienia, nie oznacza, że jest łatwiejsze w utrzymywaniu. Załóżmy, że dysponujemy monolitem, w którym implementacja usługi mieści się w kilku plikach powiązanych ze sobą i tworzą spójną całość. Jeśli zajdzie potrzeba modyfikacji kodu, naruszamy całą implementację i narażamy się na problemy w funkcjonowaniu w obszarach, których modyfikacja pozornie nie dotyczy. Jak zostanie w dalszych punktach wyjaśnione, testowanie usługi to złożony temat, więc lepiej będzie zastosować większy poziom abstrakcji i odpowiedzialność nakładać na poszczególne warstwy, niż zmuszać całość kodu do bycia mocno ze sobą powiązanym i oczekiwać pełnych testów przy najdrobniejszych zmianach.

Oczywiście, możemy w headlesowej architekturze pójść czasem na skróty, zastosować niezgodny z architekturą kod, który będzie takim proof of concept, zapewne pominie wiele negatywnych scenariuszy i pozwoli nam odpowiedzieć na biznesowe pytanie, czy usługa spełnia w praktyce nasze oczekiwania. Gdy ewentualne skutki będą akceptowane, możemy przystąpić do zbudowania solidnej implementacji, a przy okazji będziemy w posiadaniu wiedzy z jej praktycznego funkcjonowania i występujących w niej niedoskonałości.

Implementacja usług płatności

Nasz produkt działa w architekturze headlesowej i posiada zaimplementowanych wiele usług zewnętrznych. Jednymi z nich są metody płatności - usługi szczególnego typu, gdzie nie ma miejsca na błędy i niedociągnięcia, gdyż operujemy finansami użytkowników. Mimo ich dużej wagi wiele z nich można zaimplementować w ramach krótkiego kodu, który przyjmie dane z formularza i wyśle potem do pośrednika, a następnie będzie nasłuchiwał na tzw. kanale IPN, czy transakcja została opłacona, odrzucona lub uzyskała inny status. Kilka zmiennych, odebranie danych, wstawienie do bazy, kilka warunków sprawdzających obecny status oraz IP klienta na IPNie i można by uznać, że to koniec.

Niestety, wiele szczegółów wymaga dodatkowej weryfikacji, uodpornienia na negatywne scenariusze, wybrania bardziej aktualnych danych i logowania wyjątków. Przede wszystkim musimy przenieść to, co jest w dokumentacji na kod, a uzbrojeni w wiedzę i doświadczenie zadbać o następujące kwestie:

  • obsłużyć każde zapytanie frontendowe naszej aplikacji i odpowiedzieć odpowiednim komunikatem.
  • mieć pewność, kto i za co płaci, przygotować rekord na transakcję płatności i nadać jej odpowiedni status początkowy.
  • określić, jakie dane świadczą o tym, że: zamówienie jest w trakcie procesu płatności, jest opłacone, środki zostały zwrócone.
  • upewnić się, że nie są generowane powtarzalne requesty.
  • upewnić się, że przekazujemy ceny i inne dane newralgiczne jako najbardziej aktualne i pochodzące bezpośrednio z bazy danych, a nie z frontendu.
  • zawsze przekazywać odpowiedź na każdy request frontendowy.
  • statusy pojawiające się na IPNie traktować jako ważniejsze. Pomocne może być tu na przykład porównywanie dat, gdyż z tytułu opóźnień, mnogości przekierowań i komplikacji procesu dla użytkownika, prędzej możemy uzyskać informacje na IPNie niż dostać informacje z frontendu. Nieraz nawet metody płatności są tak skonstruowane, aby celowo spowolnić powrót na naszą stronę, by IPN zdążył się do nas odezwać. Z kolei, w implementacjach backendowych, możemy dostać odpowiedź później lub w podobnym czasie, co z IPNu. W tym miejscu istotne jest, by na przykład nie aktualizować stanu z nadrzędnego na podrzędny (np. “akceptacja” zmieniona na “oczekiwanie na rezultat”).
  • jeśli występuje wiele różnych statusów płatności, konieczne może być odwzorowanie grafu przejść w naszej logice biznesowej, aby prawidłowo zmieniać statusy transakcji.
  • pilnowanie liczby tworzonych transakcji i requestów w obrębie jednego zamówienia - oprócz bałaganu w bazie, możemy po prostu niepotrzebnie generować większe koszty korzystania z usługi, a i też niechlubnie narazić się operatorowi, a nawet jego dalszym pośrednikom.
  • Czasem chcemy pozwolić na drugą próbę opłacenia w sytuacji, gdy pierwsza skończyła się odrzuceniem. Jako że IPN może mieć opóźnienia w reakcji, przydatne może być w tym mechanizmie dodatkowe sprawdzanie, jakie statusy w danej chwili posiadają zarejestrowane już transakcje, zanim pozwolimy użytkownikowi na ponowną płatność.
  • zapisywanie logów z rzucanych wyjątków i skrupulatne ich neutralizowanie.

Pułapki podczas uruchamiania i utrzymywania usługi

Uruchomienie usługi i kilka pozytywnych testów na produkcji nie oznaczają, że wszystko zostało odpowiednio wdrożone, a nawet jeśli tak jest, ten stan może zmieniać się w czasie i czasem bez uprzedzenia. Warto jest czynić co jakiś czas testy na produkcji celem sprawdzenia, czy nie pojawiły się jakieś anomalie w działaniu, np. nieprzetłumaczone komunikaty, zmieniony układ elementów, które pokazujemy na frontendzie, pojawienie się nowych funkcjonalności, które nie są obsłużone, a użytkownik spodziewa się, że są.

API zmieniają się, czasem w szybkim tempie, a usługodawcy czasem potrafią wymagać od nas wysyłania dodatkowych informacji, o których dokumentacja milczy lub uznaje za niewymagane. Czasem poprawki do kodu muszą być wprowadzone szybko i wówczas mogą być konieczne zmiany na kilku warstwach naszej aplikacji.

IPN może nagle dostać nowy adres IP, z którego wysyła nam notyfikacje, może też informować nas dodatkowo o jakimś nowym statusie, na co nie byliśmy przygotowani.

Czasem też usługodawca może wygasić starą wersję API, a implementacja nowszej nie być opłacalną lub wymagać więcej czasu niż posiadamy.

Podsumowując, im lepiej zaimplementujemy usługę, lepszej jakości kod wytworzymy, tym łatwiej będzie nam reagować na zmiany, które mogą zachodzić nagle lub być zwyczajnie przeoczone.

Sandboksowe środowiska

Usługi, szczególnie dostawcy płatności, często oferują dostęp do sandboksów, w ramach których możemy testować powstającą implementację, a następnie zlecić zespołowi testerów sprawdzenie działania, także pod kątem UXowym.

Takie sandboksowe środowiska posiadają odrębne klucze dostępu i adresy url, pod które się odwołujemy. Odcięte są też np. od komunikacji z bankami na rzecz jej symulowania, przez co działają sprawniej i nie obracają prawdziwymi finansami. To sprawne działanie bywa także wadą, gdyż w realnym użyciu możemy na wysyłane requesty dostawać odpowiedzi po dłuższym czasie oczekiwania, a frontend powinien być na to gotowy. Za korzystanie z sandboksowych środowisk często też po prostu nie płacimy, dzięki czemu możemy testować mnóstwo scenariuszy.

Bywa jednak, że sandboksy działają zbyt pozytywnym scenariuszem, opierając się na tzw. “happy path”, a w praktyce, na przykład bank, ma swoje do powiedzenia (zbyt długo przetwarza dane, pokazuje inny język niż oczekiwany, uznaje coś nieprawidłowo za próbę wyłudzenia, odmawia działania z jakiegoś powodu itp.). Czasem sandboks nie pozwala też zweryfikować wszystkich przypadków negatywnych, skutkujących nieudaną płatnością.

Testowanie IPNa

Na lokalnych środowiskach nie ma możliwości lub nie powinno się dysponować możliwością odbierania zapytań z zewnątrz, stąd potrzebny jest osobny pośrednik na takie zapytania, który je zapisze (dane POST, nagłówki itp.) i następnie udostępni. Kolejno, takie zapytanie możemy wstawić do Postmana i symulować w ten sposób notyfikację IPN. Niestety, jeśli dane zawierają sygnaturę kodującą czas wysyłki lub adres url, na który były wysyłane, nie będziemy posiadali zgodności danych i powinniśmy te zapytania odrzucać. Oczywiście, można to sprawdzenie wyłączyć, ale to kolejne komplikacje w testowaniu end-to-end. Co więcej, dostawca płatności sprawdza też, czy odpowiadamy poprawnie na notyfikacje, np. kodem odpowiedzi, więc też od działania pośrednika zależy, jakie statystyki zanotuje nam panel sandboksowy. Niemniej, konieczne jest zweryfikowanie, czy IPN działa prawidłowo.

Porady w testowaniu

To, że proces skorzystania z usługi od strony użytkownika wygląda na spójny i zgodny z założeniami nie oznacza, że wszystko działa poprawnie. Powinniśmy zweryfikować także dane, które przekazujemy i odbieramy oraz sprawdzić, co zapisujemy w bazie danych. W trakcie takiej weryfikacji może się okazać, że to, co zapisujemy, niekoniecznie może w pełni odpowiadać temu, co dostajemy - i odwrotnie, nie wszystkie wysyłane dane dostawca usługi może zechcieć odebrać (lub tworzyć wyjątki, nieuwzględnione w dokumentacji).

W przypadku płatności konieczne jest sprawdzenie wszystkich rodzajów kart oraz walut, które obsługujemy. Najlepiej z małymi, jak i dużymi kwotami, aby sprawdzić reakcję na np. zabezpieczenia 3DS. Warto wykonywać w tym obszarze testy na różnych urządzeniach, niestabilnych połączeniach z internetem, kilku językach oraz automatycznym tłumaczeniu strony przez przeglądarkę.

Konieczne zatem jest weryfikowanie danych na wielu poziomach zwłaszcza jeśli zarówno frontend, jak i backend rozmawiają z usługą. Powinniśmy też na koniec sprawdzać, jakie dane znajdują się w panelu usługi - czy są zgodne z tym, co przekazaliśmy. To wszystko sprawia, że testowanie usługi i poprawianie kodu zajmują dużą jednostkę czasu w procesie całego wdrożenia, co może też rzutować na jakość kodu, dlatego też tak ważne jest, aby od początku implementacja przebiegała zgodnie ze sztuką.

Podsumowanie

Wdrażanie obsługi usług zewnętrznych niesie ze sobą wiele niewiadomych i wymaga ubezpieczania się na scenariusze negatywne oraz doraźnego reagowania, gdy zachodzą zmiany. Jeśli jednak implementacja zostanie wykonana w zadowalający sposób, ewentualne awarie łatwiej będzie usuwać. Usługa po uruchomieniu odwdzięczy się też nam bezproblemowym działaniem, a warstwowy charakter głównego projektu pozwoli na dowolność w konfiguracji i ulepszaniu, zależnie od przyszłych potrzeb.

Artykuł przygotował:
Kamil Baliński