Skip to content

t3hnar/scala-cqrs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Proof of concept - CQRS in Scala

API

  type Identifier = String
  def newIdentifier: Identifier = java.util.UUID.randomUUID().toString

  type Revision = Long


  case class AggregateRoot[A](id: Identifier, revision: Long, value: A) {
    def nextRevision(value: A): AggregateRoot[A] = copy(value = value, revision = revision + 1)
  }

  object AggregateRoot {
    def apply[A](value: A): AggregateRoot[A] = apply(newIdentifier, value)
    def apply[A](id: Identifier, value: A): AggregateRoot[A] = AggregateRoot(id, -1, value)
  }

  case class Command[C](aggregateId: Identifier, value: C)
  case class Event[E](aggregateId: Identifier, value: E)

  trait Repository[A] {
    def save(ar: AggregateRoot[A])
    def get(aggregateId: Identifier): Option[AggregateRoot[A]]
  }

  trait EventStore {
    def add(event: Event[_])
    def stream[E](aggregateId: Identifier)(implicit tag: ClassTag[E]): Stream[Event[E]]
    def stream: Stream[Event[_]]
  }

  trait Context[A, C, E] {
    def execute(command: C): E
    def applyEvent(value: A, event: E): A
    def newInstance: A
    def repository: Repository[A]
  }

  trait CommandHandler {
    def eventStore: EventStore

    def apply[A, C, E](command: Command[C])(implicit context: Context[A, C, E]): AggregateRoot[A] = {

      val aggregateId = command.aggregateId

      val ar = context.repository.get(aggregateId) getOrElse AggregateRoot(context.newInstance)

      val event = context.execute(command.value)

      eventStore.add(Event(aggregateId, event))

      val result = ar.nextRevision(context.applyEvent(ar.value, event))

      context.repository.save(result)

      result
    }
  }

Example

object InventoryItemExample {
  val eventStore: EventStore = EventStoreInMemory

  case class InventoryItem(name: String, activated: Boolean)
  sealed  trait InventoryItemEvent
  sealed trait InventoryItemCommand

  case object DeactivateInventoryItem extends InventoryItemCommand
  case class CreateInventoryItem(name: String) extends InventoryItemCommand
  case class RenameInventoryItem(newName: String) extends InventoryItemCommand
  case class CheckInItemsToInventory(count: Int) extends InventoryItemCommand
  case class RemoveItemsFromInventory(count: Int) extends InventoryItemCommand


  case object InventoryItemDeactivated extends InventoryItemEvent
  case class InventoryItemCreated(name: String) extends InventoryItemEvent
  case class InventoryItemRenamed(name: String) extends InventoryItemEvent
  case class ItemsCheckedInToInventory(count: Int) extends InventoryItemEvent
  case class ItemsRemovedFromInventory(count: Int) extends InventoryItemEvent

  object InventoryItemContext extends Context[InventoryItem, InventoryItemCommand, InventoryItemEvent] {
    def execute(command: InventoryItemCommand) = command match{
      case DeactivateInventoryItem => InventoryItemDeactivated

      case CreateInventoryItem(name) => InventoryItemCreated(name)

      case RenameInventoryItem(newName) =>
        require(newName != null)
        InventoryItemRenamed(newName)

      case CheckInItemsToInventory(count) =>
        require(count > 0)
        ItemsCheckedInToInventory(count)

      case RemoveItemsFromInventory(count) =>
        require(count > 0)
        ItemsRemovedFromInventory(count)
    }

    def applyEvent(value: InventoryItem, event: InventoryItemEvent) = event match {
      case InventoryItemDeactivated => value.copy(activated = false)
      case InventoryItemCreated(name) => value.copy(name = name)
      case InventoryItemRenamed(newName) => value.copy(name = newName)
      case ItemsCheckedInToInventory(count) => value
      case ItemsRemovedFromInventory(count) => value
    }

    def newInstance = InventoryItem("", activated = false)

    val repository = new RepositoryInMemory(eventStore, this)
  }


  val itemId = newIdentifier
  on(InventoryItemContext) handle Command(itemId, CreateInventoryItem("my item"))
  on(InventoryItemContext) handle Command(itemId, RenameInventoryItem("my renamed item"))
  on(InventoryItemContext) handle Command(itemId, CheckInItemsToInventory(2))
  on(InventoryItemContext) handle Command(itemId, RemoveItemsFromInventory(1))
  on(InventoryItemContext) handle Command(itemId, DeactivateInventoryItem)


  val handler = new CommandHandler {
    def eventStore = InventoryItemExample.eventStore
  }
  def on[A, C, E](context: Context[A, C, E]) = new {
    def handle(command: Command[C]): AggregateRoot[A] = handler(command)(context)
  }
}

Releases

No releases published

Packages

No packages published

Languages