Skip to content
Logo Theodo

Event Sourcing? What, why, how in 5 minutes

Arthur Naudy7 min read

Event Sourcing? What, why, how in 5 minutes

Event sourcing is an architectural pattern that provides a highly comprehensible audit trail.

Tracking patient medical data, tracking transactions for financial services to ensure compliance with regulatory requirements, or tracking the movement of goods in a supply chain process… Event sourcing can do it all!

As a software engineer at Theodo, I have developed a custom auction platform, which is another typical use case of event sourcing 🔨 💻

🔥 Let’s break down event sourcing using our auction platform as an example. 🔥

📄 Index :

What is event sourcing? 🧠

At the core of our Auction Platform lies Event Sourcing, a pattern where the source of truth is a list of events. By replaying these events chronologically, we dynamically reconstruct the current state of our auction, thus providing a reliable audit log for the users as well as the back office of our client.

Event Sourcing consists of capturing every change to a business entity as a series of events. In the context of our Auction Platform, this means recording every action – from the first bid on an item to the last cancellation - that modifies the current state (amount, winner, …) of the auction.

Here is a direct example: Event Sourcing Principle We apply each left event one by one. On the right side, we can see the current state is being updated at each new event.

Why did we use event sourcing❓

Event Sourcing emerged as the preferred choice for our Auction Platform due to its ability to provide a dynamic audit trail. By storing events as the primary data source, we can reconstruct the current state at any point in time.

This approach, while not without its challenges, aligns with our commitment at Theodo to tackle challenges with practical solutions.

Let’s have another look at our auction example: Event Sourcing Principle On the left side, we can see several events. The third one is a cancellation of a bid created by the backoffice.*

While Roger at one point was the winner of this auction with a $15 bid, when he connects after the end of the auction to check if he has indeed won, he realizes Joe has won with $15 instead of him.

With this audit trail, the back-office can explain to him that nothing is incorrect and that a back-office action was taken to cancel his bid for X reason, leaving room for Joe to bid with the same amount before the auction had ended.

The stored events provide a highly comprehensible audit trail, ensuring accountability.

But what if we hadn’t used Event Sourcing?

Without Event Sourcing, the first alternative would have been to store the current state of the auction directly in a database.

But by doing so, we realize that we store no information regarding the reason for the update, nor the past updates of this auction.

On the left side, we see the state of the auction. We update an auction entity directly in the database On the right side, we see the new state after Mitch’s bid. All information concerning Roger’s bid is lost

Classic Database Principle

This method, while simpler, cannot offer a detailed historical account of the actions happening during an auction. It limits transparency and hampers our ability to provide users and the back office with a clear understanding of the sequence of events that led to the current state.

So that’s that, but how does event sourcing work?

3 steps to implement your event sourcing? 🧑‍💻

Let’s walk through the flow of an event, e.g., a bid on an auction.

Here is a recap scheme that is detailed underneath from left to right:

Event Sourcing Process

Diving into the code, the action of “bidding” on an item corresponds to a POST call made to a dynamic route with the auction’s ID as a parameter.

'method' => 'POST',
'path' => '/auction-items/{itemId}/bid',
'controller' => CreateAuctionBid::class,
'input' => AuctionBidDTO::class,

with a payload AuctionBidDTO containing the amount of the bid:

final class AuctionBidDTO
{
    private int $amount;
}

and a controller CreateAuctionBid handling the call (detailed in the next part)

1. Recreate the current state by building the Aggregate

final class AuctionItemAggregate
{
    private ?int $highestBid = null;

    private int $reserveBid;

    private ?ShopUser $currentWinner = null;

    private ?ShopUser $previousWinner = null;

    private ?int $overtakenBidAmount = null;

    private \DateTime $endDate;
}

Our aggregate is the output built by replaying in sequence all the events concerning an auction. It corresponds to what we call the current state of our auction. Therefore, it contains at least the data of the current winner and the current bid amount. This aggregate is hydrated by the CreateAuctionBid controller.

For the sake of clarity, I have intentionally left only the core steps of the controller

class CreateAuctionBid extends AbstractController
{
	public function __invoke(string $itemId, AuctionBidDTO $data): AuctionBidResultDTO
    {
			$user = $this->getUser();
			$aggregate = ...->getAuctionAggregate($itemId, $user);
      .
			.
			.
    }
}

2. Create, Validate, and Apply the new incoming event

The first step is creating our event with the input data

class AuctionItemBidEvent
{
	private int $amount = 0;
	private ?AuctionItem $auctionItem = null;
	private ?User $user = null;
	...
}

We need a few classic checks to save a bid.

Is the user on a blacklist? Does the amount respect all the rules defined by the company?

so we validate the event here by checking all these domain conditions.

Then we can update our aggregate with this event by applying it.

class CreateAuctionBid extends AbstractController
{
  	public function __invoke(string $itemId, AuctionBidDTO $data): AuctionBidResultDTO
    {
      .
			.
			.
			$auctionItemBidEvent = new AuctionItemBidEvent($data, $auctionItemId, $user);
			$aggregate->validate($auctionItemBidEvent);
      $aggregate->apply($auctionItemBidEvent);
			.
			.
			.
    }
}

3. Save the event in the event store

Once all is done, we want to save our event in the database. As we saw in the introduction, we only save events in event sourcing, they are the source of truth.

class CreateAuctionBid extends AbstractController
{
  public function __invoke(string $itemId, AuctionBidDTO $data): AuctionBidResultDTO
    {
      .
			.
			.
			$aggregate->save($auctionItemBidEvent);

			$response = $this->getResult($aggregate, $user);
			return $response;
		}
}

We can then return a result (success or failure for X reason) as a response to the POST call.

👏 And that’s it! We have stored a new event 👏

Whenever we need to use the current state of an auction, we recreate its aggregate by replaying all its past events.

However, this could lead to performance issues if many aggregates with many events need to be rebuilt. We tackled this issue by saving projections of our aggregates in the database, therefore implementing a touch of Command Query Responsibility Segregation (CQRS) in our app.

Conclusion: Clean Audit Trail VS Atypical programming style 💭

Event sourcing offers a reliable audit trail and enables temporal queries. However, it requires an atypical programming style and can be challenging for querying, as it involves rebuilding the state of business entities every time. Implementing CQRS tackles this issue by storing projections of the states to query them faster.

I hope this practical overview of our implementation has given you a clear vision of the basic elements needed to get started with event sourcing! 🚀

Sources:

Liked this article?