Akka: Data Access Actors
An actor model gives us outstanding solution for building of high scale and high load systems. Those of you who work with Akka, know that in the Actor model everything should be represented as an actor. In some sense this axioma simplifies a software development. Is this circumstance as good as it seems? In this article I’m going to demonstrate some approaches which may be applied to actors which need to interact with a database. Just imagine, what sense in a high scale and high load system if it does not communicate with a database?
Input
Let’s assume that we want to implement an actor for access a person table:
//Postgres syntax CREATE TABLE person ( id serial primary key, full_name text not null, phone text, created_at timestamp not null );
What operations do we want to have for the person table? Probably a standard CRUD set of operations: create, select by id, update full_name and phone, delete by id.
Now when we know what we want, we can start doing some steps to implement the solution. Our goal is to answer the question: what is the right way to design a data access actor?
Approach #1
Everything starts from a case class:
case class Person(id: Int, fullName: String, phone: Option[String], createdAt: LocalDateTime)
Let’s assume that we have some database provider trait. In our case it may be something like PostgresDB
. It represents a driver to the database. That’s means that we can create some abstraction for data access:
trait PersonRepository { implicit val ec: ExecutionContext val db: PostgresDB def createPerson(fullName: String, phone: Option[String]): Future[Int] def getPerson(id: Int): Future[Option[Person]] def updatePerson(fullName: String, phone: Option[String]): Future[Boolean] def deletePerson(id: Int): Future[Boolean] }
Of course, now we have to create some realization of this trait:
class PersonRepositoryImpl(db: PostgresDB)(implicit val ec: ExecutionContext) extends PersonRepository { //implementation of the methods }
Wait! This article is about actors? So why we still don’t see any code related to actors? Let’s correct this somehow. The most rational idea is to define messages for the actor firstly:
object PersonDataActor { case class CreatePerson(fullName: String, phone: Option[String]) case class GetPerson(id: Int) case class UpdatePerson(fullName: String, phone: Option[String]) case class DeletePerson(id: Int) //Then we can add here response messages as a reaction on the messages declared above }
With this set of messages we can create PersonDataActor
:
class PersonDataActor(personRepo: PersonRepository) extends Actor { implicit val system = context.system implicit val ec: ExecutionContext = system.dispatcher override def receive: Receive = { case cp: CreatePerson => //corresponding function call from personRepo case gp: GetPerson => //corresponding function call from personRepo case up: UpdatePerson => //corresponding function call from personRepo case dp: DeletePerson => //corresponding function call from personRepo } }
Well. That’s it.
Is this approach good to be used in a production?
At least it works.
More significant advantage is that we can mock personRepo
for testing purposes. Hence the PersonDataActor
is testable.
Unfortunately when you need more than 1 repository for some reason, the actor’s constructor becomes “fat”.
class PersonDataActor(personRepo: PersonRepository, mobProviderRepo: MobileProviderRepository, phoneBlackListRepo: PhoneBlackListRepository) extends Actor { //... }
That’s how the things going in the approach #1.
Approach #2
I hope that you have read the previous section, because I’m going to refer to it. So why don’t we pass just one parameter to the actor’s constructor? I mean PostgresDB
. If we do so, this makes the actor construction more elegant, because all the repositories can be initialized inside of the actor:
class PersonDataActor(postgresDB: PostgresDB) extends Actor { val personRepo = new PersonRepositoryImpl(postgresDB) val mobProviderRepo = new MobileProviderRepositoryImpl(postgresDB) val phoneBlackListRepo = new PhoneBlackListRepositoryImpl(postgresDB) //... }
Does this approach better than the first one? Actually no, because “elegance” of PersonDataActor
constructor gives you less than takes back. This code is hard to test: you are not able to mock the repositories as you need according to test scenarios. So you will need to create in-memory DB for each test suite run.
Summary
I tried to highlight the problems, which may occur with data access actors, when you design your actor system. Definitely this article is just a top of iceberg. Maybe I miss something when try to make separation of concerns in context of actors. Any way, I’ll be really glad to read about your experience in this area.
How would you implement this data access actor?