├── .gitignore ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ └── application.conf └── scala │ └── net │ └── branchandbound │ └── eventsourcing │ ├── ConcertActor.scala │ ├── ConcertHistoryView.scala │ ├── ConcertMain.scala │ ├── ConcertProtocol.scala │ └── CounterActor.scala └── test └── scala └── net └── branchandbound └── eventsourcing └── ConcertActorSpec.scala /.gitignore: -------------------------------------------------------------------------------- 1 | *# 2 | *.iml 3 | *.ipr 4 | *.iws 5 | *.pyc 6 | *.tm.epoch 7 | *.vim 8 | */project/boot 9 | */project/build/target 10 | */project/project.target.config-classes 11 | *-shim.sbt 12 | *~ 13 | .#* 14 | .*.swp 15 | .DS_Store 16 | .cache 17 | .cache 18 | .classpath 19 | .codefellow 20 | .ensime* 21 | .eprj 22 | .history 23 | .idea 24 | .manager 25 | .multi-jvm 26 | .project 27 | .scala_dependencies 28 | .scalastyle 29 | .settings 30 | .tags 31 | .tags_sorted_by_file 32 | .target 33 | .worksheet 34 | Makefile 35 | TAGS 36 | lib_managed 37 | logs 38 | project/boot/* 39 | project/plugins/project 40 | src_managed 41 | target 42 | tm*.lck 43 | tm*.log 44 | tm.out 45 | worker*.log 46 | /bin 47 | /journal 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Example code for 'Event-sourcing with Akka' 2 | 3 | You need Scala 2.11 and SBT to build and run this code. The file 'ConcertMain.scala' starts up the persistent actor and view and runs a small scenario. 4 | 5 | Since the default LevelDB journal of Akka is used, the events are stored in ```journal``` directory. You can remove the directory to start over with a clean eventstore. 6 | 7 | The application can be run using SBT: 8 | ``` 9 | sbt run 10 | ``` 11 | 12 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | val akkaVersion = "2.3.6" 2 | 3 | resolvers += "krasserm at bintray" at "http://dl.bintray.com/krasserm/maven" 4 | 5 | val project = Project( 6 | id = "concerts-akka-persistence", 7 | base = file("."), 8 | settings = Project.defaultSettings ++ Seq( 9 | name := "concerts-akka-persistence", 10 | version := "1.0", 11 | scalaVersion := "2.11.2", 12 | libraryDependencies ++= Seq( 13 | "com.typesafe.akka" %% "akka-contrib" % akkaVersion, 14 | "com.github.michaelpisula" %% "akka-persistence-inmemory" % "0.2.1", 15 | "org.scalatest" %% "scalatest" % "2.1.6" % "test", 16 | "commons-io" % "commons-io" % "2.4" % "test") 17 | ) 18 | ) -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.5 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | 2 | resolvers += Classpaths.typesafeResolver 3 | 4 | addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0") 5 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka.persistence.journal.leveldb.dir = "target/example/journal" 2 | akka.persistence.snapshot-store.local.dir = "target/example/snapshots" 3 | 4 | # DO NOT USE THIS IN PRODUCTION !!! 5 | # See also https://github.com/typesafehub/activator/issues/287 6 | akka.persistence.journal.leveldb.native = false -------------------------------------------------------------------------------- /src/main/scala/net/branchandbound/eventsourcing/ConcertActor.scala: -------------------------------------------------------------------------------- 1 | package net.branchandbound.eventsourcing 2 | 3 | import akka.persistence.PersistentActor 4 | import akka.actor.ActorLogging 5 | import java.util.Date 6 | import akka.persistence.SnapshotOffer 7 | import akka.actor.Props 8 | 9 | object ConcertActor { 10 | import ConcertProtocol._ 11 | 12 | sealed trait Command 13 | case class BuyTickets(user: String, quantity: Int) extends Command 14 | case class ChangePrice(newPrice: Int) extends Command 15 | case class AddCapacity(newCapacity: Int) extends Command 16 | case class CreateConcert(price: Int, available: Int, startTime: Date) extends Command 17 | 18 | sealed trait Query 19 | case object GetSalesRecords extends Query 20 | 21 | case class ConcertState(price: Int, available: Int, startTime: Date, sales: Seq[SalesRecord] = Nil) { 22 | def updated(evt: ConcertEvent): ConcertState = evt match { 23 | case TicketsBought(user, quant) => copy(price, available - quant, startTime, 24 | SalesRecord(user, quant, price) +: sales) 25 | case PriceChanged(newPrice) => copy(price = newPrice, available, startTime, sales) 26 | case CapacityIncreased(toBeAdded) => copy(price, available = available + toBeAdded, startTime, sales) 27 | case _ => this 28 | } 29 | } 30 | case class SalesRecord(user: String, quantity: Int, price: Int) 31 | 32 | def props(id: String) = Props(new ConcertActor(id)) 33 | 34 | } 35 | 36 | class ConcertActor(id: String) extends PersistentActor with ActorLogging { 37 | 38 | import ConcertActor._ 39 | import ConcertProtocol._ 40 | 41 | def persistenceId = "Concert." + id 42 | 43 | var state: Option[ConcertState] = None 44 | 45 | def updateState(evt: ConcertEvent) = state = state.map(_.updated(evt)) 46 | 47 | def setInitialState(evt: ConcertCreated) = { 48 | state = Some(ConcertState(evt.price, evt.available, evt.startTime)) 49 | context.become(receiveCommands) 50 | } 51 | 52 | val receiveRecover: Receive = { 53 | case evt: ConcertCreated => setInitialState(evt) 54 | case evt: ConcertEvent => updateState(evt) 55 | case SnapshotOffer(_, snapshot: ConcertState) => { 56 | state = Some(snapshot) 57 | context.become(receiveCommands) 58 | } 59 | } 60 | 61 | val receiveCreate: Receive = { 62 | case c@CreateConcert(price, available, startTime) => { 63 | persist(ConcertCreated(price, available, startTime)) { evt => 64 | println(s"Creating concert with from message $c") 65 | setInitialState(evt) 66 | } 67 | } 68 | } 69 | 70 | val receiveCommands: Receive = { 71 | case BuyTickets(user, quant) if quant <= state.get.available => { 72 | persist(TicketsBought(user, quant))(evt =>{ 73 | println(s"Selling $quant tickets to '$user'") 74 | updateState(evt) 75 | sender() ! evt 76 | }); 77 | } 78 | case BuyTickets(user, _) => { 79 | persist(SoldOut(user))(evt => { 80 | println("Sold out!") 81 | updateState(evt) 82 | sender() ! evt 83 | }) 84 | } 85 | case ChangePrice(newPrice) => { 86 | persist(PriceChanged(newPrice)){ 87 | println(s"Price changed to $newPrice") 88 | evt => updateState(evt) 89 | sender() ! evt 90 | } 91 | } 92 | case AddCapacity(toBeAdded) => { 93 | persist(CapacityIncreased(toBeAdded)){ 94 | println(s"Capacity increased with $toBeAdded") 95 | evt => updateState(evt) 96 | sender() ! evt 97 | } 98 | } 99 | 100 | 101 | // Queries 102 | case GetSalesRecords => { 103 | sender() ! state.get.sales 104 | } 105 | } 106 | 107 | // Initially we expect a CreateConcert command 108 | val receiveCommand: Receive = receiveCreate 109 | 110 | } -------------------------------------------------------------------------------- /src/main/scala/net/branchandbound/eventsourcing/ConcertHistoryView.scala: -------------------------------------------------------------------------------- 1 | package net.branchandbound.eventsourcing 2 | 3 | import akka.actor.Props 4 | import akka.persistence.PersistentView 5 | import collection.immutable.ListSet 6 | 7 | object ConcertHistoryView { 8 | import ConcertProtocol._ 9 | 10 | case class ConcertHistoryState(priceHistory: ListSet[Int] = ListSet.empty, 11 | ticketsPerPrice: Map[Int, Int] = Map.empty.withDefaultValue(0), 12 | soldOuts: Int = 0) { 13 | 14 | def updated(evt: ConcertEvent): ConcertHistoryState = evt match { 15 | case ConcertCreated(price, _, _) => copy(priceHistory + price, ticketsPerPrice, soldOuts) 16 | case SoldOut(_) => copy(priceHistory, ticketsPerPrice, soldOuts + 1) 17 | case TicketsBought(_, quantity) => { 18 | val currentPrice = priceHistory.head 19 | val newTicketsPerPrice = ticketsPerPrice.updated(currentPrice, ticketsPerPrice(currentPrice) + quantity) 20 | copy(priceHistory, newTicketsPerPrice, soldOuts) 21 | } 22 | case PriceChanged(newPrice) => copy(priceHistory + newPrice, ticketsPerPrice, soldOuts) 23 | case CapacityIncreased(_) => this 24 | } 25 | } 26 | 27 | case object GetConcertHistory 28 | 29 | def props(id: String) = Props(new ConcertHistoryView(id)) 30 | } 31 | 32 | class ConcertHistoryView(id: String) extends PersistentView { 33 | 34 | import ConcertHistoryView._ 35 | import ConcertProtocol._ 36 | 37 | override def persistenceId: String = "Concert." + id 38 | override def viewId: String = "Concert.view" + id 39 | 40 | var state = ConcertHistoryState() 41 | 42 | def receive: Receive = { 43 | case evt: ConcertEvent if isPersistent => state = state.updated(evt) 44 | case GetConcertHistory => sender() ! state; 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/scala/net/branchandbound/eventsourcing/ConcertMain.scala: -------------------------------------------------------------------------------- 1 | package net.branchandbound.eventsourcing 2 | 3 | import java.util.Date 4 | import scala.concurrent.Await 5 | import scala.concurrent.duration.DurationInt 6 | import ConcertActor.BuyTickets 7 | import ConcertActor.CreateConcert 8 | import ConcertActor.GetSalesRecords 9 | import ConcertActor.SalesRecord 10 | import ConcertActor.props 11 | import akka.actor.ActorSystem 12 | import akka.actor.actorRef2Scala 13 | import akka.pattern.ask 14 | import akka.util.Timeout 15 | import akka.persistence.Update 16 | 17 | object ConcertMain extends App { 18 | 19 | import ConcertActor._ 20 | import ConcertHistoryView._ 21 | 22 | val system = ActorSystem("concert-es") 23 | val concertActor = system.actorOf(ConcertActor.props("concert1"), "concert1Actor") 24 | val concertHistoryActor = system.actorOf(ConcertHistoryView.props("concert1"), "concert1HistoryView") 25 | 26 | val create = CreateConcert(50, 100, new Date) 27 | val buy = BuyTickets("me", 50) 28 | val change = ChangePrice(75) 29 | concertActor ! create 30 | println(s"------- ConcertActor -----> $create") 31 | concertActor ! buy 32 | println(s"------- ConcertActor -----> $buy") 33 | concertActor ! change 34 | println(s"------- ConcertActor -----> $change") 35 | 36 | // Retrieve and print the SalesRecords from ConcertActor 37 | implicit val timeout = Timeout(5 seconds) 38 | val salesRecordsFuture = concertActor ? GetSalesRecords 39 | println(s"------- ConcertActor -----> $GetSalesRecords") 40 | val salesRecords = Await.result(salesRecordsFuture, timeout.duration) 41 | println(s"<------ ConcertActor ------ $salesRecords") 42 | 43 | // Since PersistenView polls (eventual consistency) we force it 44 | // to sync with the journal for our demo 45 | concertHistoryActor ! Update(await = true) 46 | 47 | // Retrieve and print the ConcertHistory 48 | val historyFuture = concertHistoryActor ? GetConcertHistory 49 | println(s"---- ConcertHistoryView ---> GetConcertHistory") 50 | val history = Await.result(historyFuture, timeout.duration) 51 | println(s"Answer from ConcertHistoryView: $history") 52 | println(s"<--- ConcertHistoryView ---- history") 53 | 54 | system.shutdown() 55 | } -------------------------------------------------------------------------------- /src/main/scala/net/branchandbound/eventsourcing/ConcertProtocol.scala: -------------------------------------------------------------------------------- 1 | package net.branchandbound.eventsourcing 2 | 3 | import java.util.Date 4 | 5 | object ConcertProtocol { 6 | 7 | sealed trait ConcertEvent 8 | case class ConcertCreated(price: Int, available: Int, startTime: Date) extends ConcertEvent 9 | case class SoldOut(user: String) extends ConcertEvent 10 | case class TicketsBought(user: String, quantity: Int) extends ConcertEvent 11 | case class PriceChanged(newPrice: Int) extends ConcertEvent 12 | case class CapacityIncreased(toBeAdded: Int) extends ConcertEvent 13 | 14 | } -------------------------------------------------------------------------------- /src/main/scala/net/branchandbound/eventsourcing/CounterActor.scala: -------------------------------------------------------------------------------- 1 | 2 | package net.branchandbound.eventsourcing 3 | 4 | import akka.persistence.PersistentActor 5 | import akka.persistence.SnapshotOffer 6 | import akka.persistence.PersistentView 7 | 8 | case object Increment 9 | case object Incremented 10 | 11 | class CounterActor extends PersistentActor { 12 | 13 | def persistenceId = "counter" 14 | 15 | var state = 0 16 | 17 | val receiveCommand: Receive = { 18 | case Increment => persist(Incremented) { evt => 19 | state += 1 20 | println("incremented") 21 | } 22 | } 23 | 24 | val receiveRecover: Receive = { 25 | case Incremented => state += 1 26 | } 27 | } 28 | 29 | class SnapshottingCounterActor extends PersistentActor { 30 | def persistenceId = "snapshotting-counter" 31 | 32 | var state = 0 33 | 34 | val receiveCommand: Receive = { 35 | case Increment => persist(Incremented) { evt => 36 | state += 1 37 | println("incremented") 38 | } 39 | case "takesnapshot" => saveSnapshot(state) 40 | } 41 | 42 | val receiveRecover: Receive = { 43 | case Incremented => state += 1 44 | case SnapshotOffer(_, snapshotState: Int) => state = snapshotState 45 | } 46 | } 47 | 48 | case object ComplexQuery 49 | 50 | class CounterView extends PersistentView { 51 | override def persistenceId: String = "counter" 52 | override def viewId: String = "counter-view" 53 | 54 | var queryState = 0 55 | 56 | def receive: Receive = { 57 | case Incremented if isPersistent => { 58 | queryState = someVeryComplicatedCalculation(queryState) 59 | // Or update a document/graph/relational database 60 | } 61 | case ComplexQuery => { 62 | sender() ! queryState; 63 | // Or perform specialized query on datastore 64 | } 65 | } 66 | 67 | def someVeryComplicatedCalculation(state: Int) = 42 68 | } -------------------------------------------------------------------------------- /src/test/scala/net/branchandbound/eventsourcing/ConcertActorSpec.scala: -------------------------------------------------------------------------------- 1 | package net.branchandbound.eventsourcing 2 | 3 | import akka.testkit.TestKit 4 | import akka.actor.ActorSystem 5 | import com.typesafe.config.ConfigFactory 6 | import org.scalatest.Matchers 7 | import akka.testkit.ImplicitSender 8 | import org.scalatest.BeforeAndAfterAll 9 | import org.scalatest.WordSpecLike 10 | import java.util.Date 11 | import net.branchandbound.eventsourcing.ConcertProtocol.TicketsBought 12 | import net.branchandbound.eventsourcing.ConcertProtocol.SoldOut 13 | 14 | class ConcertActorSpec(_system: ActorSystem) extends TestKit(_system) with ImplicitSender with WordSpecLike with Matchers with BeforeAndAfterAll { 15 | def this() = this( 16 | ActorSystem("TestActorSystem", ConfigFactory.parseString( 17 | """ 18 | |akka.loglevel = "DEBUG" 19 | |akka.persistence.journal.plugin = "in-memory-journal" 20 | |akka.actor.debug { 21 | | receive = on 22 | | autoreceive = on 23 | | lifecycle = on 24 | |} 25 | """.stripMargin))) 26 | 27 | override def afterAll() { 28 | TestKit.shutdownActorSystem(system) 29 | } 30 | import ConcertActor._ 31 | "A ConcertActor" when { 32 | val getSalesCmd = GetSalesRecords 33 | 34 | s"receiving '$getSalesCmd'" should { 35 | s"return empty list of sales" in { 36 | val concertActor = _system.actorOf(ConcertActor.props("1")) 37 | concertActor ! CreateConcert(10, 100, new Date) 38 | concertActor ! getSalesCmd 39 | 40 | expectMsg(List()) 41 | } 42 | } 43 | 44 | val buyCmd = BuyTickets("me", 1) 45 | s"receiving '$buyCmd'" should { 46 | s"return Success" in { 47 | val concertActor = _system.actorOf(ConcertActor.props("1")) 48 | concertActor ! buyCmd 49 | 50 | expectMsg(TicketsBought("me", 1)) 51 | } 52 | } 53 | 54 | s"receiving '$buyCmd' and then '$getSalesCmd'" should { 55 | s"return non-empty list of sales" in { 56 | val concertActor = _system.actorOf(ConcertActor.props("2")) 57 | concertActor ! CreateConcert(1, 100, new Date) 58 | concertActor ! buyCmd 59 | concertActor ! getSalesCmd 60 | 61 | expectMsg(TicketsBought("me", 1)) 62 | expectMsg(Seq(SalesRecord("me", 1, 1))) 63 | } 64 | } 65 | 66 | val buyTooMuchCmd = BuyTickets("me", 100) 67 | s"receiving '$buyTooMuchCmd'" should { 68 | s"return Success" in { 69 | val concertActor = _system.actorOf(ConcertActor.props("3")) 70 | concertActor ! CreateConcert(1, 1, new Date) 71 | concertActor ! buyTooMuchCmd 72 | 73 | expectMsg(SoldOut("me")) 74 | } 75 | } 76 | 77 | } 78 | } --------------------------------------------------------------------------------