Spis treści

Co to jest Event Sourcing?

Event Sourcing to idea, w której stan obiektu wynika bezpośrednio z atomicznych zdarzeń, które na ten obiekt wpływają. Dzięki zaimplementowaniu wzorca możemy mieć pewność, że właściwości obiektu nie zmienią się bez powodu, czyli bez określonego w modelu domeny konkretnego działania. Od samego momentu powołania obiektu do życia znamy jego pełną historię, co stanowi naturalny audit log, który może być kluczowy w niektórych wrażliwych obszarach aplikacji, jak na przykład obsługa konta walutowego użytkownika.

Właśnie to zastosowanie - konto bankowe - będę w tym artykule wykorzystywał jako przykład.

Event Sourcing to bardzo prosta koncepcja, która dobrze użyta może przynieść ogromne korzyści przy niewielkim koszcie jej obsługi My przekonaliśmy się o tym przy pracy nad projektem tipply.pl, w którym Event Sourcing zastosowaliśmy do obsługi kont na napiwki użytkowników.

Zdarzenia

Zdarzenia składające się na stan obiektu magazynujemy w tzw. Event Store. Implementacja magazynu jest tutaj dowolna - może to być relacyjna baza danych MySQL, nierelacyjna NoSQL czy jakikolwiek inny sposób, który uznamy za najlepiej odpowiadający naszej aplikacji. Istnieją również rozwiązania dedykowane właśnie do realizacji magazynowania zdarzeń, takie jak na przykład EventStoreDB.

Co istotne, żeby maksymalnie kontrolować złożoność systemu, zdarzenia powinny być konkretne, zawierające czyste, precyzyjne dane. Na podstawie tych informacji będziemy później decydowali w jaki sposób zmienić stan aplikacji. Na przykład zasilenie konta spowoduje zwiększenie właściwości “saldo” o kwotę zdefiniowaną w zdarzeniu. Wskazane jest też, żeby dane te były zapisane w najprostszy sposób, najlepiej wykorzystując zwykłe obiekty lub jeszcze lepiej - wartości skalarne. W ten sposób dane istotne dla zdarzenia będziemy mogli zapisać używając formatów, takich jak JSON, XML itp. To z kolei ułatwi nam przekazywanie tych informacji na przykład z użyciem brokera wiadomości (RabbitMQ itp.).

W naszym przykładzie, możemy zdefiniować następujące zdarzenia:

  • OpenAccount - otwarcie konta; jest to pierwsze zdarzenie, powołujące nasz obiekt do życia; dane: {"balance": 0, "status": "OPEN"}
  • DepositMoney - zasilenie konta daną kwotą; dane: {"amount": 100, "title": "Wpłata 1 zł"}
  • WithdrawMoney - wypłata danej kwoty; dane: {"amount": 50, "title": "Wypłata środków", "method": "bank_transfer"}
  • CloseAccount - zamknięcie konta; dane: {"old_status": "OPEN", "new_status": "CLOSED"}

Do czego Event Sourcing może zostać użyty?

Użycie architektury Event Sourcingu ma wiele naturalnych zalet. Pierwszą (oczywistą z nich) jest audit log, zawierający pełną informację o historii danego obiektu.

Dzięki zachowaniu wszystkich zdarzeń możemy cofnąć się do dowolnego momentu w historii. Na przykład, chcąc sprawdzić stan konta z poprzedniego miesiąca, wystarczy odtworzyć wszystkie zdarzenia do określonego momentu. Ta cecha jest szczególnie przydatna w przypadku debugowania. W klasycznym podejściu prawdopodobnie nie moglibyśmy tego zrobić, ponieważ zachowywalibyśmy wyłącznie stan obecny.

Jeśli wykryjemy błąd w jakimś przeszłym zdarzeniu, możemy to wydarzenie wyszukać w Event Store, a potem w łatwy sposób odwrócić skutki błędu, dokonując operacji odwrotnej. Jeśli jest to bezpieczne i usprawiedliwione, możemy nawet to zdarzenie usunąć i ponownie zaaplikować zdarzenia, które wystąpiły po nim.

Te i inne zalety Event Sourcingu obszernie opisał jeden z największych autorytetów inżynierii oprogramowania - Martin Fowler w swoim artykule, w którym nazwał i przedstawił opisywany przeze mnie wzorzec.

Typowe zastosowania:

  • konto bankowe,
  • system kontroli wersji,
  • śledzenie historii pojazdów w firmie transportowej,
  • historia zamówienia produktu.

Projekcje

Z istoty architektury Event Sourcingu wynika fakt, że aby odtworzyć obecny stan obiektu, musimy po kolei wywołać wszystkie zdarzenia, które go dotyczą. Nie stanowi to problemu dla obiektów, które mają krótki czas życia lub które są jeszcze młode, a więc zdarzeń do przetworzenia nie ma zbyt wielu. Nie można też wykluczyć, że w zależności od logiki biznesowej, modyfikacja stanu nie jest trywialna i zaaplikowanie zdarzenia jest relatywnie zasobożerne. Jest jednak wiele sposobów na to, żeby zoptymalizować aplikację.

W zdecydowanej większości przypadków, przy odpytywaniu aplikacji interesuje nas jedynie bieżący stan obiektu. Z pomocą przychodzą projekcje. Dzięki nim możemy stworzyć Read Model opisujący najnowszy stan obiektu. Przykładowo możemy stworzyć ciągły proces, który czeka na wpłynięcie nowego wydarzenia - kiedy to nastąpi, aktualizuje rekord w bazie danych zawierające informacje o bieżącym stanie obiektu. Wtedy, zamiast odtwarzać wszystkie zdarzenia, wystarczy odwołać się do bazy danych.

Projekcje mogą też być przydatne w innych, rzadziej występujących przypadkach, do realizacji mniej popularnych zadań. Na przykład w aplikacji firmy zajmującej się handlem morskim, możemy przetworzyć zdarzenia w Event Store w celu wyłonienia wszystkich statków, które co najmniej raz odwiedziły Rotterdam. Jak widać, elastyczność, którą daje Event Sourcing, jest ogromna.

Event sourcing

Implementacja

Działanie Event Sourcingu możemy przedstawić za pomocą prostego testu:

public function testOpensAccount()
{
  $account = new Account();
  $this->eventProcessor->addEvent(new OpenAccount($account, "OPEN", 0));
  $this->assertEquals("OPEN", $account->status);
  $this->assertEquals(0, $account->balance);
}

public function testDeposit()
{
  $account = new Account();
  $this->eventProcessor->addEvent(new OpenAccount($account, "OPEN", 0));
  $this->eventProcessor->addEvent(new DepositMoney($account, 100, "Wpłata 1 zł"));
  $this->assertEquals(100, $account->balance);
}

public function testWithdraw()
{
  $account = new Account();
  $this->eventProcessor->addEvent(new OpenAccount($account, "OPEN", 0));
  $this->eventProcessor->addEvent(new DepositMoney($account, 100, "Wpłata 1 zł"));
  $this->eventProcessor->addEvent(new DepositMoney($account, 200, "Wpłata 2 zł"));  
  $this->eventProcessor->addEvent(new WithdrawMoney($account, 50, "Wypłata środków", "bank_transfer"));
  $this->assertEquals(250, $account->balance);
}

Prosty kod PHP umieściłem na GitHubie. Ta implementacja jest oczywiście bardzo prymitywna. Istnieje jednak wiele gotowych, wysokiej jakości open-sourcowych bibliotek implementujących wzorzec Event Sourcing. Oczywiście warto przy tym pamiętać, że nasza logika biznesowa nie powinna zależeć od biblioteki, której użyjemy (podejście framework-agnostic).