Domain models

With event-sourcing, we want to store changes of the state as events. Not every interaction with the client is an event. Until we know that we can comply, we're modeling it as a command. Specifically, in our example it is represented as sealed traits:

sealed trait Command
sealed trait Query

object Commands {
final case class CreateArticle(name: String, count: Int) extends Command
final case class DeleteArticle(name: String) extends Command
final case class PurchaseArticles(order: Map[String, Int]) extends Command
final case class RestockArticles(stock: Map[String, Int]) extends Command
final case object GetInventory extends Query
}

In the spirit of CQRS, we model incoming data as four commands and one query. The commands can be made into the events if the current state allows that:

object Events {
final case class ArticleCreated(name: String, count: Int) extends Event
final case class ArticleDeleted(name: String) extends Event
final case class ArticlesPurchased(order: Map[String, Int]) extends Event
final case class ArticlesRestocked(stock: Map[String, Int]) extends Event
}

In our simple case, commands and events correspond to each other, but in the real project, this won't always be the case. 

We also have a representation of the current state of the store:

final case class Inventory(state: Map[String, Int]) extends Persistable { ... }

Inventory extends Persistable so that we can make snapshots later. We will keep the business logic separate from the actor-related code. Because of this, our inventory should be able to handle events itself:

def update(event: Event): Inventory = event match {
case ArticleCreated(name, cnt) => create(name, cnt).get
case ArticleDeleted(name) => delete(name).get
case ArticlesPurchased(order) => add(order.mapValues(_ * -1))
case ArticlesRestocked(stock) => add(stock)
}

The create method adds an article to the store and assigns some initial counts to it if possible. It returns an inventory in the new state in the case of success:

def create(name: String, count: Int): Option[Inventory] =
state.get(name) match {
case None => Some(Inventory(state.updated(name, count)))
case _ => None
}

The delete method tries to delete an article from the inventory:

def delete(name: String): Option[Inventory] =
if (state.contains(name))
Some(Inventory(state.filterKeys(k => !(k == name))))
else None

The add method sums the count of articles from another inventory with counts of all articles existing in this inventory:

def add(o: Map[String, Int]): Inventory = {
val newState = state.foldLeft(Map.empty[String, Int]) {
case (acc, (k, v)) => acc.updated(k, v + o.getOrElse(k, 0))
}
Inventory(newState)
}

Now our inventory can accept events and return itself in an updated state, but we still have to deal with commands first. One possible implementation of the logic for command-handling could look like this:

def canUpdate(cmd: Command): Option[Event] = cmd match {
case CreateArticle(name, cnt) =>
create(name, cnt).map(_ => ArticleCreated(name, cnt))
case DeleteArticle(name) => delete(name).map(_ => ArticleDeleted(name))
case PurchaseArticles(order) =>
val updated = add(order.mapValues(_ * -1))
if (updated.state.forall(_._2 >= 0)) Some(ArticlesPurchased(order)) else None
case RestockArticles(stock) => Some(ArticlesRestocked(stock))
}

The canUpdate method takes a command and returns a corresponding event in the case that it is possible to apply the command successfully. For creating and deleting articles, we're checking that the operation will produce a valid result; for purchases, we're checking that there are enough articles in stock, and restock should always succeed.

Our Inventory is not synchronized and hence it is not safe to work within a concurrent scenario. Moreover, if one thread makes modifications to the inventory at the time another thread already called canUpdate, but has not called update yet, we might end up with the incorrect state because of this race condition. But we don't need to worry about that because we're going to use our inventory inside of an actor.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.188.154.252