Event Store czyli Magazyn Zdarzeń
Nowoczesne aplikacje wymagają rozwiązań zapewniających łatwość ich utrzymania oraz skalowania. Takie możliwości może nam pomóc uzyskać Magazyn Zdarzeń. W artykule tym opowiem co to takiego, jakie korzyści nam daje i jak możemy go zastosować.

Magazyn zdarzeń
Magazyn Zdarzeń (ang. Event Store) to najprościej mówiąc dziennik w którym zapisujemy zdarzenia. Zacznijmy więc od wyjaśnienia czym są zdarzenia (events).
Załóżmy że mamy aplikację sklepu, w której ktoś dodaje produkty do koszyka, a następnie dokonuje zamówienia. Mając interfejs Event oraz implementujące go klasy ItemAdded, ItemRemoved, OrderPaid, OrderCompleted, możemy tworzyć egzemplarze tych klas w odpowiednich miejscach naszego programu. Możemy też zaimplementować Listenery, czyli klasy lub/i metody, które wykonują kolejne zadania gdy pojawi się event danego rodzaju, np. po opłaceniu zamówienia (event OrderPaid) wysyłamy e-mail z potwierdzeniem płatności.
Zatem nasz kod informuje o różnego rodzaju wydarzeniach tworząc obiekty zdarzeń a inne części kodu, zdarzeń tych nasłuchują. Rozwiązanie takie jest popularne nawet w aplikacjach, które nie używają magazynowania zdarzeń.
Wdrożenie magazynu zdarzeń daje nam jednak nowe możliwości rozwoju aplikacji, dodawania nowych funkcjonalności oraz zarządzania stanem aplikacji.
Przykładowy scenariusz
Mamy sklep RTV.
Nasz klient dodał do koszyka produkt Dysk SSD Seagate 256GB, ale po chwili się rozmyślił i go usunął, a zamiast niego dodał Dysk SSD GoodRam 256GB. Dodatkowo do koszyka wrzucił Kabel HDMI 0,5m.
Ostatecznie koszyk klienta będzie wyglądał tak:
- Dysk SSD GoodRam 256GB
- Kabel HDMI 0,5m
W bazie danych możemy go zapisać w prostej tabeli zawierającej ID klienta, ID produktu i ilość:
| ClientId | ProductId | Quantity | | 765 | 56 | 1 | | 765 | 784 | 1 |
Ten sam scenariusz z event store
Możemy jednak podejść do zagadnienia nieco inaczej i zamiast zapisywać skład koszyka, zapisać poszczególne działania użytkownika:
- [ Dodano Dysk SSD Seagate 256GB ]
- [ Usunięto Dysk SSD Seagate 256GB ]
- [ Dodano Dysk SSD GoodRam 256GB ]
- [ Dodano Kabel HDMI 0,5m ]
Załóżmy że zdarzenie dodania produktu będziemy nazywać ItemAdded, natomiast usunięcie produktu ItemRemoved.
Mając tak zapisany dziennik zdarzeń jesteśmy w stanie odtworzyć końcową zawartość koszyka, dokładnie taką jak w pierwszym przykładzie, ale też jesteśmy w stanie zrobić dużo więcej, łącznie z cofaniem się w czasie 🙂
Odbudowanie agregatu
Odbudowanie agregatu polega na zbudowaniu jego aktualnego stanu, na podstawie eventów zapisanych w magazynie. W naszym przykładzie agregatem jest koszyk. Wyciągając z magazynu wszystkie eventy w kolejności chronologicznej i stosując je na koszyku możemy odtworzyć końcowy stan koszyka. Dodatkowo mamy możliwość odtworzenia stanu koszyka w dowolnym momencie w przeszłości. Oczywiście ten “dowolny moment” musi się zawierać w okresie czasu od momentu gdy zaczęliśmy zapisywać zdarzenia do teraz.
Snapshoty
Odtwarzanie agregatu można zoptymalizować stosując snapshoty. Snapshot to utrwalony stan agregatu na dany moment, do którego możemy następnie zaaplikować kolejne eventy. Dzięki zastosowaniu snapshotów nie musimy zawsze odtwarzać agregatów od początku historii zdarzeń, jeśli akurat nie ma takiej potrzeby.
Implementacja nowych funkcjonalności
Załóżmy że Product Owner zgłosił się do nas z potrzebą zaimplementowania raportu na którym zaprezentowane będzie jakie produkty klienci usuwali z koszyka w danym okresie czasu. Któż zrozumie potrzeby biznesu? 😉
W przypadku gdy koszyk zapisujemy w tabeli, jak w naszym pierwszym przykładzie, nie będziemy w stanie wygenerować raportu dla historycznych danych, możemy jedynie gromadzić dane od momentu wprowadzenia zliczania i tylko te dane prezentować na raporcie.
W przypadku gdy mamy zapisaną historię zdarzeń, możemy użyć zdarzeń z przeszłości do zbudowania stanu aplikacji uwzględniając nowe funkcjonalności. Mówiąc prościej: możemy wygenerować raport dla wszystkiego tego co się wydarzyło od początku istnienia naszego event store.
Zbieramy dane do raportu
W celu wygenerowania raportu usuwanych produktów, potrzebujemy informacji jakie produkty były usuwane z koszyka i ile razy. Musimy więc każdy event ItemRemoved zaaplikować do klasy generującej raport.
Przykładowo wywołania mogą wyglądać następująco:
applyItemRemoved(Item(“Dysk SSD Seagate 256GB”)) applyItemRemoved(Item(“Słuchawki nauszne PHILIPS TAPH805BK”)) applyItemRemoved(Item(“Telewizor LG 55UP81003LR”)) applyItemRemoved(Item(“Smartband HUAWEI Band 6”)) applyItemRemoved(Item(“Słuchawki nauszne PHILIPS TAPH805BK”))
W momencie odbudowywania raportu z historycznych danych, wywołania metody applyItemRemoved będą dotyczyły eventów ItemRemoved pobranych z magazynu. Następie, w normalmym działaniu aplikacji eventy ItemRemoved będą się pojawiały na bieżąco i od razu będą aplikowane do klasy raportującej – dzięki temu będziemy w stanie aktualizować raport na bieżąco.
Projekcje
Nasza klasa raportująca, zawiera kolekcję informacji o usuniętych produktach. Kolekcja ta może być zapisywana do tabeli w bazie danych, z której inne części aplikacji (a nawet inne aplikacje) mogą czytać dane – wówczas taką klasę nazywamy projektorem, a wypełnioną w bazie tabelę – projekcją.
W naszym przykładzie projekcja może wyglądać tak:
| Produkt | Ile razy usunięto? | | Dysk SSD Seagate 256GB | 1 | | Słuchawki nauszne PHILIPS TAPH805BK | 2 | | Telewizor LG 55UP81003LR | 1 | | Smartband HUAWEI Band 6 | 1 |
Projekcji możemy użyć do gromadzenia danych potrzebnych do konkretnych zastosowań, bez potrzeby wyszukiwania ich pośród gąszczu innych informacji. Przykładowo, chcemy mieć w naszym systemie tabelkę wyświetlającą nazwiska i adresy naszych klientów zebrane ze złożonych zamówień. Czy do wyświetlenia takiej tabeli musimy zaimplementować kod, który w momencie złożenia zamówienia będzie zapisywał potrzebne dane w bazie danych? Niezupełnie. Wystarczy, że stworzymy projektor, który w momencie wystąpienia zdarzenia zawierającego dane klienta (np. OrderPlaced) wyrysujemy na projekcji nazwisko i adres klienta. Różnica między bezpośrednim zapisywaniem danych w momencie składania zamówienia, a wyrysowywaniem ich na projekcji w reakcji na event jest znów taka, że jesteśmy w stanie zrobić to dla danych historycznych.
Możemy również w prosty sposób dodawać kolejne pola do raportowanych informacji, dodając je do projektora i odbudowując projekcję, na przykład gdy biznes zgłosi nam potrzebę, żeby obok adresu wysyłki podawać również adres e-mail – wyświetlimy go wówczas we wszystkich pozycjach raportu, również tych historycznych.
Dziennik
Kolejną zaletą magazynu zdarzeń jest to że jest on jednocześnie dziennikiem tego co dzieje się w naszej aplikacji, co bywa bardzo pomocne w debugowaniu aplikacji, czy też w analizie biznesowej zaimplementowanych procesów. Taki log funkcjonuje najlepiej wtedy, gdy cała nasza aplikacja opiera się na eventach i dokładnie każde zdarzenie zapisuje się w event store.
Załóżmy, że w naszej aplikacji mamy błąd, który objawia się kiedy dodamy produkt do koszyka, następnie go z koszyka usuniemy i dodamy jeszcze raz. Czy mając zapisany tylko końcowy stan koszyka będziemy w stanie odtworzyć zachowanie użytkownika? Nie. Natomiast mając dziennik jakim jest event store, jesteśmy w stanie to zrobić.
Kolejka zdarzeń
Magazyn zdarzeń nie musi być tylko dziennikiem, zapisującym co się w naszym systemie wydarzyło. Wręcz przeciwnie, może być sercem całego systemu, jeśli użyjemy go jako kolejkę zdarzeń. Oznacza to, że wszystko co się dzieje w systemie będzie zgłaszane do event store, a dalsze procesy będą uruchamiane w reakcji na te zgłoszenia, zatem każdy element systemu będzie albo zapisywał do dziennika albo z niego nasłuchiwał.
Przywracanie stanu aplikacji po awarii
Co się stanie jeśli po wydaniu nowej funkcjonalności okaże się, że jakaś część systemu nie działa prawidłowo, na przykład dane na raportach są prezentowane w nieprawidłowy sposób? Kod w repozytorium możemy naprawić lub cofnąć do określonego stanu, ale co z danymi? Otóż mając zapisaną historię zdarzeń możemy po wprowadzeniu poprawek w kodzie odbudować stan agregatów czy projekcji, uzyskując w ten sposób prawidłowe dane. Nie byłoby to możliwe gdybyśmy dane trzymali w sposób “klasyczny”, czyli np. bezpośrednio w tabelach bazy danych, które po aktualizacji rekordu zapominają o jego poprzednim stanie.
Implementacja magazynu zdarzeń
Dobrze zaimplementowany event store powinien być niezależny od infrastruktury, abyśmy mogli na przykład używać bazy danych do przechowywania dziennika na produkcji, a w środowisku testowym używać rozwiązania trzymającego dziennik w pamięci.
Dziennik zdarzeń powinien być też niezależny od logiki biznesowej, powinien zapisywać dowolne typy eventów wraz z zawartymi w nich informacjami, również takie które powstaną w przyszłości, a o których jeszcze nie wiemy.
Event store warto implementować od razu z całą “obudową” w postaci rozgłaszania eventów do klas nasłuchujących czy aplikowania ich do agregatów.
Jeśli chcesz zaimplementować event store w istniejącej aplikacji to pamiętaj, że jeśli nie jest ona zbudowana w oparciu o eventy, to event store nie da ci maksimum możliwości, jeśli nie przepiszesz aplikacji tak żeby wszystko opierało się na zdarzeniach.
Podsumowanie
W artykule tym starałem się zaprezentować czym jest Magazyn Zdarzeń i jakie możliwości daje. Jeśli udało mi się ciebie zainteresować tematem, to jako dalszy krok nauki polecam dobrą książkę o DDD lub analizę kodu projektu w którym event store jest już stosowany, jeśli masz taką możliwość w pracy, na uczelni lub po prostu znalazłeś dobry przykład open source.
Skomentuj