Skip to Content
Course content

68: Akka Persistence for Event Sourcing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think about how a professional accountant manages a ledger. They don't just erase the total balance and write a new number every time a transaction happens. That would be a nightmare for auditing. Instead, they record every single single entry: "Deposit $100", "Withdraw $20", "Payment $50". If the accountant loses their place or the book gets damaged, they can start from zero and replay every single transaction to arrive at the exact current balance. This is the essence of Event Sourcing.

In Akka, we use Akka Persistence to do exactly this for our actors. Instead of saving the actor's current state to a database (which is what we call "State Persistence"), we save the sequence of events that led to that state. If the actor crashes or the JVM restarts, Akka reads those events back from a journal and "replays" them to restore the actor to its last known state.

Commands are requests, Events are facts

One thing that trips people up when they first start with Event Sourcing is the distinction between a Command and an Event. I like to think of it as the difference between asking for something and recording that it happened.

  • Command: "Please withdraw $20." (This can be rejected if you have insufficient funds).
  • Event: "Withdrew $20." (This is a historical fact. It cannot be undone or rejected because it has already happened).

In your code, your actor receives a Command, validates it against the current state, and if everything looks good, it persists an Event. Only after the event is safely written to the journal do you actually update the actor's internal state.

Replaying the past to find the present

Let's look at how this actually looks in a EventSourcedBehavior. I'll use a simple Bank Account example because it maps perfectly to the ledger analogy.

import akka.persistence.typed.scaladsl.*;
import akka.persistence.typed.domain.*;

object BankAccount {
  // The State
  case class State(balance: Int) {
    def applyEvent(event: Event): State = event match {
      case Deposited(amount) => copy(balance = balance + amount)
      case Withdrawn(amount) => copy(balance = balance - amount)
    }
  }

  // Commands (Requests)
  sealed trait Command
  case class Deposit(amount: Int) extends Command
  case class Withdraw(amount: Int) extends Command

  // Events (Facts)
  sealed trait Event
  case class Deposited(amount: Int) extends Event
  case class Withdrawn(amount: Int) extends Event

  def apply(): Behavior[Command] = EventSourcedBehavior[Command, Event, State](
    persistenceId = "account-123",
    emptyState = State(0),
    commandHandler = (state, cmd) => cmd match {
      case Deposit(amount) => 
        Effect.persist(Deposited(amount))
      case Withdraw(amount) if state.balance >= amount => 
        Effect.persist(Withdrawn(amount))
      case Withdraw(_) => 
        Effect.none // Reject the command! No event is persisted.
    },
    eventHandler = (state, event) => state.applyEvent(event)
  )
}

Notice the flow here: the commandHandler doesn't change the balance. It only decides if an event should be persisted. The eventHandler is the only place where the state actually changes. This separation is what makes Event Sourcing so powerful—you have a perfect audit log of every decision the system ever made.

Dealing with the "Infinite Log" problem

Now, you might be thinking: "If my actor lives for five years and processes a million transactions, won't it take forever to restart because it has to replay a million events?"

You're absolutely right. That's where Snapshots come in. A snapshot is essentially a "save game" of your actor's state at a specific point in time. Instead of replaying from event #1, Akka can load the snapshot from event #10,000 and then only replay the events that happened after that snapshot. It's a massive performance win for long-lived actors.




📋 Practical Task

Implementation: Persistent Order Tracking System

Your task is to implement a persistent actor that tracks the lifecycle of an e-commerce order. An order can be created, paid for, and shipped.

Requirements:

  • State: Create an OrderState that tracks the current status (e.g., Created, Paid, Shipped) and a list of items.
  • Commands: Implement CreateOrder(items: List[String]), PayOrder, and ShipOrder.
  • Events: Implement OrderCreated, OrderPaid, and OrderShipped.
  • Validation Logic:
    • An order cannot be paid for unless it has been created.
    • An order cannot be shipped unless it has been paid for.
    • If a command is invalid based on the current state, it should result in Effect.none.

Write the EventSourcedBehavior for this system, ensuring that the eventHandler correctly transitions the state through the order lifecycle.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.