Implementing CQRS with Akka

Michał Tomański

@michaltomanski

CQRS

What is it about?

  • Command Query Responsibility Segregation
  • Design/architecture pattern
class UserService {
  createUser(User user): Unit
  getUser(long id): User
}
class UserWriteService {
  createUser(User user): Unit
}

class UserReadService {
  getUser(long id): User
}

And that's basically it

Let's mix it!

Event Sourcing

  • The state of an object is equivalent to all past events applied one by one
  • Persist all the events to a journal
  • Events are immutable, they are facts
  • Events can be replayed to restore the state

Event Sourcing

  • System receives command
  • Command gets validated
  • Command generates events
  • Events get persisted
  • Events are applied on the state

DeactivateUser(id = 1)

Is the user with id=1 active?

UserDeactivated(id = 1)

UserDeactivated(id = 1) goes to DB

isActive = false

Why?

  • Persist facts
  • Write only, never modify/delete
  • Better domain modelling
  • Temporal queries
  • Free audit logs / BI data

- Cool, but how do we query this thing? 

- Well, you don't

Why bother?

  • Everything said earlier
  • Suitable database engines for each side
  • Independent read and write side scalability
  • Independent data replication factor
  • Read or write oriented domain
  • Better performance with prepared views
  • Loose coupling

Let's build something!

Awesome Timer

®

  • Measures the time
  • Online
  • Users see other users' results (rivalry)

Overview

Awesome Timer

®

Write side

  • Accept user's new time
  • Remove the user's time (in case of unintentionally stopping the timer)
  • Add +2s (penalty)

Rare

Awesome Timer

®

Read side

Show to the user his:

  • Best single time
  • Best average of 5
  • Best average of 12
  • Best average of 100
  • Current average of 5, 12, 100
  • Last 100 times

Show to the user other people's:

  • Best single time
  • Best average of 5
  • Best average of 12
  • Best average of 100
  • Current average of 5, 12, 100

Awesome Timer

®

Read side

Average of 5: 16, (20), 18, 18, (15) 

= (16 + 18 + 18) /3

Awesome Timer

®

Writes and reads

  • Extremely simple write side (just persist the username and the time), but with (hopefully) a lot of traffic
  • Quite complicated and expensive reads (calculating best average of 5 among thousands of entries, querying multiple users)
  • Typical CRUD is not the case here

- Ok, how do we do that? We build all of those persistence, projections etc?

  • A toolkit focused on actors model
  • Useful for concurrent, distributed and message driven systems
  • High-level abstraction

Actor

  • Encapsulate state and behavior
  • React to a message
  • Supervision

Actor

class Speedcuber extends Actor { 
  var times = Nil

  def receive = {
    case AddTime(time: Long) => times :+ time
  }
}

actor ! AddTime(500)

Persistent Actor

  • Persist every valid event to the journal
  • Do your business logic after persistence
  • Replay all the events when needed

Persistent Actor

class Speedcuber extends PersistentActor {
  var times = Nil

  override def receiveCommand: Receive = {
    case addTime: AddTime =>
      persist(addTime) { event =>
        times = times :+ event.time
      }
  }  
  
  override def receiveRecover: Receive = {
    case addTime: AddTime => times = times :+ event.time
    case RecoveryCompleted => println("Events recovery completed")
  }

  override def persistenceId: String = "speedcuber"
}

Persistent Actor

As a domain entity

Persistent Actor

Journal (Cassandra)

Persistent Actor

Storage plugins

  • Cassandra, DynamoDB, Mongo, SQL etc.
  • Storage initialization done automagically by the plugin, lazily or eagerly
  • Specified in .conf file
akka.persistence.journal.plugin = "cassandra-journal"
akka.persistence.snapshot-store.plugin = "cassandra-snapshot-store"

Projection

Akka Persistence Query as a projection

Akka persistence query

Postgresql

Projection

Akka Persistence Query as a projection

  • Get source of events by tag from journal
  • Starting from last offset
  • Map the stream with async function
  • At the end, save the offset of the event
  • When handling the event, construct the view and upsert to the database
val s = readJournal.eventsByTag("Speedcuber")
val s = readJournal.eventsByTag("Speedcuber", state.offset)
s.mapAsync(1)(handleEvent).runWith(Sink.foreach(offset => self ! SaveOffset(offset)))

def handleEvent = {
  case EventEnvelope(offset, _, _, event: BestAvgChanged) =>
    viewBuilder.upsertBestAvgView(BestAvg(event.user, event.millis)).map(_ => offset)
val s = readJournal.eventsByTag("Speedcuber", state.offset)
val s = readJournal.eventsByTag("Speedcuber", state.offset)
s.mapAsync(1)(handleEvent)
val s = readJournal.eventsByTag("Speedcuber", state.offset)
s.mapAsync(1)(handleEvent).runWith(Sink.foreach(offset => self ! SaveOffset(offset)))

Cluster Singleton

Projection as a Cluster Singleton

The oldest node

Controllers

Communicating with the world

Eventual consistency

Should I care?

  • Read data will be consistent with what is inside the event store... eventually.
  • Getting an updated view by the user might take a while
  • But isn't it always the case?
  • Data is not wrong, it's just old

Cubing time

Live demo

Summary

We have a service...

  • That can have read and write model scaled independently
  • With databases suitable for read and write
  • Easy to integrate with other services via events messaging
  • Easy to be scaled horizontally with state recovered
  • With complete and immutable events history
  • With prepared expensive views
  • Easy to be tested and debugged (just dump events from prod)

Implementing CQRS with Akka

Michał Tomański

@michaltomanski

slides.com/michaltomanski/cqrs

github.com/michaltomanski/cqrs-demo

vote.scalaua.com

Challenges

Eventual consistency

Should I care?

  • Well, there might be a problem while making domain validations against views...
  • When? When not?
  • Send a compensation event
  • Tweak with polling times
  • Keep track of inconsistency issues
  • ... Or do not design your (micro)service this way

¯\_(ツ)_/¯

Cluster sharding

As a domain entities sharding

Node 1

Node 2

Events evolution

  • We want an actor and the projection to always work on the newest events versions
  • Events stored in journal can be upcasted to the newest version during replay by adapters
  • Good serialization method may also help

Entities startup

  • Use rememberEntities flag
  • Rebalancing shards to another nodes
  • Startup of the application
  • Journal might get overwhelmed 
  • Use snapshotting
  • Passivation after idle timeout

Serialization

  • Domain events persistence
  • Internal events persistence (e.g. shard coordinator)
  • Remoting for domain events
  • Remoting for internal events

Remoting

  • Akka is designed for remoting
  • Just a matter of configuration

Persistent Actor

Event adapters

  • Tagging - e.g. fetching only domain events
  • Events versioning
event-adapters = {
  speedcuber = "com.mtomanski.timer.infrastructure
    .akka.adapter.SpeedcuberEventsTaggingAdapter"
}

event-adapter-bindings = {
  "com.mtomanski.timer.domain.model.Speedcuber" = [speedcuber]
}
class SpeedcuberEventsTaggingAdapter extends WriteEventAdapter {
  override def toJournal(event: Any): Any = Tagged(event, Set("Speedcuber"))
  ... 
}

Copy of CQRS

By liosedhel

Copy of CQRS

  • 1,669