Reputation: 9335
I'm experimenting with Event Sourcing with Scala (I'm new both in this two fields).
I'd like to keep everything as immutable as possible, including aggregate roots. As a base, I follow Greg Young's example of ES and CQRS (https://github.com/gregoryyoung/m-r, implemented in C#) and 'translating' this code into Scala.
Question is about object regeneration from event store:
Since my aggregate roots are immutable, for each event of particular entity, new object is created. So, for example, to regenerate entity which has 100 events, 100 instances of this entity should be created and last instance will be the final state.
Is it efficient or is to too much overhead?
(I think max events to regenerate from will be 100, since I will store snapshots).
Here is my code:
Event trait
trait Event {
}
Base class for aggregates
abstract class AggregateRoot {
...
// HERE ENTITY IS REGENERATED FROM EVENTS AND EACH TIME NEW INSTANCE IS CREATED FOR EACH ENTITY
def loadFromHistory(history: Seq[Event]): AggregateRoot = {
var aggregate = this
history.foreach(e => aggregate = applyChange(e, false))
aggregate
}
def applyChange(event: Event, isNew: Boolean): AggregateRoot
...
}
Product Aggregate
case class Product(id: Long, name: String, status: Int, uncommittedChanges: Seq[Event]) extends AggregateRoot {
def changeName(name: String): Product = {
applyChange(ProductNameChangedEvent(id = this.id, name = name))
}
def applyChange(event: Event, isNew: Boolean = true): Product = {
val changedProduct = event match {
case e: ProductCreatedEvent => applyChange(e)
case e: ProductNameChangedEvent => applyChange(e)
case _ => throw new Exception("No event applier")
}
// ALSO HERE I'M COPING ALL THE LOCAL (NEW) CHANGES (THAT ARE NOT STORED IN EVENT STORE YET)
if (isNew) changedProduct.copy(uncommittedChanges = uncommittedChanges :+ event) else changedProduct
}
def applyChange(event: ProductCreatedEvent): Product = {
this.copy(id = event.id, name = event.name)
}
def applyChange(event: ProductNameChangedEvent): Product = {
this.copy(id = event.id, name = event.name)
}
...
}
Upvotes: 0
Views: 344
Reputation: 13985
Well... when you talk about efficiency, it will always come-down to the choices of your data-structures and how-what are you optimising ?
In this particular case your Product
looks like this,
case class Product(id: Long, name: String, status: Int, changes: Seq[Event])
And as you are accumulating your events in changes
, your efficiency will be determined by the data structure here. You can not just leave it with a general Seq
.
Now there is the important question which you should ask - what are your use-cases for this changes
?
Well... other than all un-known things... I can see that whenever you receive a new event you want to add it to your changes
. Which means that if you are receiving vary frequent events than you need to optimise the operation of adding it to your changes
So... lets say you were using changes: List[Event]
,
Now, you will have to use prepend
which is constant time - C instead of append
which is O(n).
// so this line
if (isNew) changedProduct.copy(changes = changes :+ event) else changedProduct
// will have to changed to
if (isNew) changedProduct.copy(changes = event +: changes) else changedProduct
But now you changes
list will contain events in the reversed order... so you will have to keep in mind that you apply them in reverse order when rebuilding your state.
To know more about the time-complexity of some operations on various collections, you should take a look at this - http://docs.scala-lang.org/overviews/collections/performance-characteristics.html
Just, remember that "efficiency" on its on it has no meaning. You should always ask - "efficiency... but in doing what ?"
Upvotes: 1