Tworzone aplikacje często muszą informować swoich użytkowników o jakimś zdarzeniu poprzez SMS czy email. Powodem może być wystawienie faktury, potwierdzenie złożenia rezerwacji czy też nowa promocja. Co jednak w przypadku, gdy chcemy mieć pewność, że wiadomość nie dotrze do adresata jeśli wystąpi błąd w trakcie przetwarzania żądania? Albo że otrzymane dane nie zapiszą się, gdy wyskoczy wyjątek podczas doręczania komunikatu? Rozwiązaniem może być zastosowanie wzorca Transactional Outbox.

Opis problemu

Jak zawsze zacznijmy od studium przypadku. Nasza aplikacja pobiera opłatę od klienta za zakup wybranych produktów. W tym samym czasie wysyła wiadomość do zewnętrznego systemu, aby je zarezerwować. Tak jak napisałem we wstępie, nie chcemy, aby pobrano opłatę jeśli wystąpi błąd podczas wysyłki rezerwacji. Również druga sytuacja jest niekorzystna, gdy towar zostanie zabukowany, ale klient za niego nie zapłacił z racji błędu.

Płatność nie została zarejestrowana pomimo tego, że wiadomość nie została wysłana
Płatność została zarejestrowana pomimo tego, że rezerwacja nie została wysłana

Wiadomość została wysłana pomimo tego, że płatność nie została zarejestrowana
Rezerwacja została wysłana pomimo tego, że płatność nie została zarejestrowana

Jeśli obydwa zagadnienia są kluczowe dla biznesu i nie powinny zadziać się jedno w odosobnieniu od drugiego to powyższe rozwiązanie jest niedopuszczalne. Trzeba zastanowić się czy istnieje jakieś inne podejście do tego problemu.

Wzorzec Transactional Outbox

Przeglądając Internet natrafimy na wzorzec Transactional Outbox, który może okazać się bardzo przydatny. Zamiast wrzucać wiadomość do brokera możemy ją zapisać do bazy danych w ramach tej samej transakcji co płatność. Następnie przy wykorzystaniu schedulera moglibyśmy co jakiś czas pobierać dostępne wiadomości i je wysyłać. W ten sposób ograniczamy rozsynchronizowanie się procesu.


Schemat działania wzorca Transactional Outbox

Sporym plusem takiego rozwiązania jest brak możliwości wysyłki rezerwacji bez zarejestrowania płatności. Jeśli wystąpi błąd podczas jednego z dwóch procesów to transakcja wycofa nowe wpisy z bazy danych. Również kolejność zapisywanych informacji powinna zostać zachowana. Natomiast minusem może okazać się fakt, że w ramach domeny wkrada się logika infrastrukturalna. Ten argument może być trochę naciągany, ale niektóry mogliby mieć co do tego pewne wątpliwości.

Oczywiście Transactional Outbox nie rozwiąże wszystkich bolączek. Zastanówmy się nad sytuacją, w której ktoś dokonał płatności i rezerwacji w prawidłowy sposób. Po jakiejś chwili MessageService zaciągnie informację o zabukowanym produkcie i wyśle ją do zewnętrznego systemu. Jednak dany artykuł nie jest już dostępny, więc otrzymujemy błąd. Wynikiem tego zajścia jest to, że odbyła się rejestracja płatności, a produkt nie jest do dyspozycji klienta. W tym momencie można zastanowić się nad dwoma rozwiązaniami.

Pierwszym z nich jest zastosowanie operacji kompensującej. W momencie, gdy dostaniemy komunikat, że rezerwacja nie jest możliwa możemy wycofać płatność poprzez np. zapytanie DELETE. Jest to podejście zastosowania kompensacji rozproszonej transakcji znane chociażby z wzorca Sagi. Drugim sposobem jest zastanowienie się czy być może źle nie rozdzieliliśmy kontekstów domeny. Możliwe, że dzięki kilku technikom i niewygodnym pytaniom dojdziemy do wniosku, że trzeba na nowo podzielić kod aplikacji.

Podsumowanie

Liczę na to, że tym krótkim wpisem przybliżyłem Ci ideę działania wzorca Transactional Outbox. Jest on jedną z opcji do rozważenia podczas rozmów zespołowych na temat architektury. Czasami jednak może okazać się przerostem formy nad treścią. Gdyby nie chodziło o tak ważne zdarzenie jakim jest rezerwacja produktu to niekoniecznie zależałoby nam na takiej atomowości. Wyobraź sobie, że w ramach operacji zakupu trzeba przy okazji poinformować klienta mailowo o dodatkowych promocjach. Być może to zajście nie jest kluczowe dla naszego biznesu. Wtedy wystarczyłoby jako pierwszą operację dać zapis płatności do bazy danych, a później wysyłkę. Gdy pobranie opłaty się nie powiedzie mail nie dotrze do klienta. Natomiast, gdy sytuacja będzie odwrotna to płatność się zarejestruje, ale klient nie dowie się o promocji. I to byłoby w niektórych sytuacjach jak najbardziej w porządku.