Software Architect | Scala Lover | Creator of Caliban and Shardcake
Nothing here yet.
Right, we are not using Akka persistence and instead use our own code. We're just using https://github.com/typelevel/doobie to communicate with the database. We don't use STM in this part of the code actually, it's all one big state object in memory. When we load an actor, we load the latest snapshot and journals, replay those to have the latest state in memory. Then we serve reads and writes from that. When we need a "fast access" to some data without loading the actor, we have some view tables in the database that are updated via Kafka: after the state is updated and saved in DB we publish some Kafka messages and a separate component reads those and update the views.
We do use event sourcing and events are written to DB only (we use Kafka to trigger some workflows but with different events and much smaller scale). So our DB usage is very write-heavy indeed. We use CockroachDB which makes it quite easily to scale. Event sourcing is independent from Shardcake: Shardcake provides us actors/entities that are distributed over our servers and addressable by ID, then in each entity we use event sourcing to manage the entity state (via snapshots/journals). So we use both but they are not directly connected.
The way we do it is that we have a function liftEvent that "logs" the event but also runs a "transition" that modifies the state. We use a typeclass for Transition[Event] . When running the ZPure we get the new state with the list of lifted events and we can write those event to our event store. On replay, we run the same transitions for each event.
Since they return ZIO , you can't use them directly. In our case we often include the current time in the environment R (so we get it before running the ZPure and then the value is consistent during the execution since the environment is read-only). For random, we use Scala Random directly wrapped inside ZPure.suspend .
This allows you to handle the actor messages in parallel instead of sequentially (e.g. using mapZIOPar or just forking each message handling). Now depending on the use case that may or may not be desirable. If there would be a lot of contention (each request handling accessing the same objects), sequentially is probably better. In that case you can still use the ZIO/ZPure separation and skip the ZSTM layer. In my use cases (games), we often want to respond to client requests ASAP so parallelizing is better.