Akka 2.3 introduces akka-persistence, a wonderful way of implementing event-sourced applications. Let's give it a shot and see how DDD and Akka are a match made in heaven :-)
14. sourcing styles
Command Sourcing Event Sourcing
msg: DoThing
msg persisted before receive
imperative, “do the thing”
business logic change,
can be reflected in reaction
Processor
15. sourcing styles
Command Sourcing Event Sourcing
msg: DoThing msg: ThingDone
msg persisted before receive
commands converted to events,
must be manually persisted
imperative, “do the thing” past tense, “happened”
business logic change,
can be reflected in reaction
business logic change,
won’t change previous events
Processor EventsourcedProcessor
60. Processor
import akka.persistence._!
!
class CounterProcessor extends Processor {!
var count = 0!
override val processorId = "counter"!
!
def receive = {!
case notPersisted =>!
// will not replay this msg!!
count += 1!
}!
}
counter ! payload
won’t persist
61. Processor
import akka.persistence._!
!
class CounterProcessor extends Processor {!
var count = 0!
override val processorId = "counter"!
!
def receive = {!
case notPersisted =>!
// will not replay this msg!!
count += 1!
}!
}
counter ! payload
won’t persist
won’t replay
62. Processor
import akka.persistence._!
!
class CounterProcessor extends Processor {!
var count = 0!
override val processorId = "counter"!
!
def receive = {!
case Persistent(payload, seqNr) =>!
// payload already persisted!
count += 1!
!
case notPersistentMsg =>!
// msg not persisted - like in normal Actor!
count += 1!
}!
}
63. Processor
Upsides
• Persistent Command Sourcing “out of the box”
• Pretty simple, persist handled for you
• Once you get the msg, it’s persisted
• Pluggable Journals (HBase, Cassandra, Mongo, …)
• Can replay to given seqNr (post-mortem etc!)
64. Processor
Downsides
• Exposes Persistent() to Actors who talk to you
• No room for validation before persisting
• There’s one Model, we act on the incoming msg
• Lower throughput than plain Actor (limited by DB)
68. super quick domain modeling!
sealed trait Command!
case class ManyCommand(nums: List[Int]) extends Command
Commands - input from user,“send emails”, not persisted
69. super quick domain modeling!
sealed trait Command!
case class ManyCommand(nums: List[Int]) extends Command
Commands - input from user,“send emails”, not persisted
sealed trait Event!
case class AddOneEvent(num: Int) extends Event!
Events - business events emitted by the processor, persisted
70. super quick domain modeling!
sealed trait Command!
case class ManyCommand(nums: List[Int]) extends Command
Commands - input from user,“send emails”, not persisted
case class State(count: Int) {!
def updated(more: Int) = State(count + more)!
}
State - internal actor state, kept in var
sealed trait Event!
case class AddOneEvent(num: Int) extends Event!
Events - business events emitted by the processor, persisted
81. EventsourcedProcessor
class MultiCounter extends EventsourcedProcessor {!
!
var state = State(count = 0)!
!
def updateState(e: Event): State = {!
case event: AddOneEvent => state.updated(event.num)!
}!
!
// API:!
!
def receiveCommand = ??? // TODO!
!
def receiveRecover = ??? // TODO!
!
}!
82. EventsourcedProcessor
def receiveCommand = {!
case ManyCommand(many) =>!
for (event <- many map AddOneEvent)!
persist(event) { state = updateState(_) }!
!
case command: Command =>!
persist(AddOneEvent(command)) { state = updateState(_) }!
}
83. EventsourcedProcessor
def receiveCommand = {!
case ManyCommand(many) =>!
for (event <- many map AddOneEvent)!
persist(event) { state = updateState(_) }!
!
case command: Command =>!
persist(AddOneEvent(command)) { state = updateState(_) }!
}
84. EventsourcedProcessor
def receiveCommand = {!
case ManyCommand(many) =>!
for (event <- many map AddOneEvent)!
persist(event) { state = updateState(_) }!
!
case command: Command =>!
persist(AddOneEvent(command)) { state = updateState(_) }!
}
85. EventsourcedProcessor
def receiveCommand = {!
case ManyCommand(many) =>!
for (event <- many map AddOneEvent)!
persist(event) { state = updateState(_) }!
!
case command: Command =>!
persist(AddOneEvent(command)) { state = updateState(_) }!
}
async persist
that event
86. EventsourcedProcessor
def receiveCommand = {!
case ManyCommand(many) =>!
for (event <- many map AddOneEvent)!
persist(event) { state = updateState(_) }!
!
case command: Command =>!
persist(AddOneEvent(command)) { state = updateState(_) }!
}
async persist
that event
async called after
successful persist
87. EventsourcedProcessor
def receiveCommand = {!
case ManyCommand(many) =>!
for (event <- many map AddOneEvent)!
persist(event) { state = updateState(_) }!
!
case command: Command =>!
persist(AddOneEvent(command)) { state = updateState(_) }!
}
async persist
that event
async called after
successful persist
It is guaranteed that no new commands will be received by a processor
between a call to `persist` and the execution of its `handler`.
88. EventsourcedProcessor
def receiveCommand = {!
case ManyCommand(many) =>!
for (event <- many map AddOneEvent)!
persist(event) { state = updateState(_) }!
!
case command: Command =>!
persist(AddOneEvent(command)) { state = updateState(_) }!
}
91. EventsourcedProcessor
case ManyCommand(many) =>!
for (event <- many map AddOneEvent)!
persist(event) { state = updateState(_) }
case ManyCommand(many) =>!
persist(many map AddOneEvent) { state = updateState(_) }
“style fix”
92. Eventsourced, recovery
/** MUST NOT SIDE-EFFECT! */!
def receiveRecover = {!
case replayedEvent: Event => !
updateState(replayedEvent)!
}
121. Snapshots
Downsides
• More logic to write
• Maybe not needed if events replay “fast enough”
• Possibly “yet another database” to pick
• snapshots are different than events, may be big!