-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
159: Building an Order Processing System with Akka Persistence
By now, you're comfortable with Akka actors, but in a real-world production system, "in-memory" isn't enough. If your node crashes, your state is gone. You might be tempted to just throw a database call inside your actor, but that's a recipe for bottlenecks and consistency nightmares. This is where Akka Persistence comes in. Instead of saving the current state, we save the sequence of events that led to that state. This is Event Sourcing.
Defining our Order Domain
I want us to build a simple Order Processing system. We aren't just tracking a total; we're tracking the lifecycle of an order. I'll start by defining the commands (what we want to do) and the events (what actually happened). It's a subtle distinction, but it's the most important part of persistence.
case class OrderItem(sku: String, price: Double)
// Commands: The "Intent"
sealed trait OrderCommand
case class CreateOrder(orderId: String, customerId: String) extends OrderCommand
case class AddItem(item: OrderItem) extends OrderCommand
case class ShipOrder() extends OrderCommand
// Events: The "Fact"
sealed trait OrderEvent
case class OrderCreated(orderId: String, customerId: String) extends OrderEvent
case class ItemAdded(item: OrderItem) extends OrderEvent
case class OrderShipped() extends OrderEvent
// State: The "Result"
case class OrderState(
orderId: Option[String] = None,
items: List[OrderItem] = Nil,
isShipped: Boolean = false
)
Building the EventSourcedBehavior
Now, let's wire this up. We use EventSourcedBehavior. The magic here is the separation between the commandHandler (which decides if an action is allowed) and the eventHandler (which actually updates the state). I like to think of the command handler as the "gatekeeper" and the event handler as the "bookkeeper."
import akka.persistence.typed.scaladsl.{Effect, EventSourcedBehavior}
import akka.persistence.typed.PersistenceId
object OrderActor {
def apply(orderId: String): Behavior[OrderCommand] = {
EventSourcedBehavior[OrderCommand, OrderEvent, OrderState](
persistenceId = PersistenceId.ofUniqueId(orderId),
emptyState = OrderState(),
commandHandler = (state, cmd) => cmd match {
case CreateOrder(id, _) =>
Effect.persist(OrderCreated(id, "cust-123"))
case AddItem(item) if !state.isShipped =>
Effect.persist(ItemAdded(item))
case ShipOrder() if state.items.nonEmpty =>
Effect.persist(OrderShipped())
case _ =>
Effect.ignore // Or send a failure message back to the sender
},
eventHandler = (state, event) => event match {
case OrderCreated(id, _) => state.copy(orderId = Some(id))
case ItemAdded(item) => state.copy(items = state.items :+ item)
case OrderShipped() => state.copy(isShipped = true)
}
)
}
}
A mistake I almost left in: The Side-Effect Trap
Here is where I usually trip up when I'm moving too fast. I originally tried to trigger the "Shipping Email" notification inside the eventHandler. It looked like this:
// WRONG WAY
eventHandler = (state, event) => event match {
case OrderShipped() =>
EmailService.sendShippingConfirmation(state.orderId) // DANGER!
state.copy(isShipped = true)
}
I realized my mistake during a restart test. Because Akka Persistence replays every single event from the journal to rebuild the state when an actor starts, my customer suddenly received five shipping emails because the actor crashed and recovered five times. Never put side-effects in the event handler. The event handler must be a pure function.
To fix this, I move the side-effect to the commandHandler using Effect.persist combined with a callback, or better yet, by handling the logic after the event has been successfully persisted.
Handling Side Effects Correctly
If you need to do something in the real world (like hitting an API) after an event is saved, you use the Effect.persist block's ability to execute a callback. Here is the corrected ShipOrder logic:
case ShipOrder() if state.items.nonEmpty =>
Effect.persist(OrderShipped())
.thenRun {
// This only runs once when the event is first persisted,
// NOT during recovery.
EmailService.sendShippingConfirmation(state.orderId)
}
Now the system is robust. We have a permanent audit log of everything that happened to the order, the state is recoverable, and we aren't spamming our customers every time the JVM restarts.
📋 Practical Task
Implement Order Validation and Refund Logic
Your task is to extend the OrderActor system we built to handle refunds, but with a strict business rule: an order cannot be refunded if it hasn't been shipped yet.
- Create a new command
RequestRefundand a corresponding eventOrderRefunded. - Update the
OrderStateto include aisRefunded: Booleanfield. - Implement the logic in the
commandHandlerto ensureRequestRefundonly persists an event ifstate.isShippedis true andstate.isRefundedis false. - Update the
eventHandlerto update the state whenOrderRefundedoccurs. - Add a side-effect using
.thenRunthat logs "Processing payment return for order X" only when the refund is successfully persisted.
There are no comments for now.