Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Akka: Data Access Actors

DZone's Guide to

Akka: Data Access Actors

This experiment explores using Akka's actors for data access, the problems that approach encounters and some appropriate workarounds.

· Java Zone ·
Free Resource

Download Microservices for Java Developers: A hands-on introduction to frameworks and containers. Brought to you in partnership with Red Hat.

An actor model gives us an outstanding solution for the 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 axiom simplifies software development. Is this circumstance as good as it seems? In this article, I’m going to demonstrate some approaches that may be applied to actors that need to interact with a database. Just imagine, what is the sense of 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 accessing 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, and delete by id.

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 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, isn't it? So why don’t we see any code related to actors? Let’s correct this somehow. The most rational idea is to define messages for the actor first:

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 response messages here 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? Well, at least it works. The 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)

  //...
}


Is this approach better than the first one? Actually no, because “elegance” of PersonDataActor constructor gives you less than it takes back. This code is hard to test: You are not able to mock the repositories as you need to according to test scenarios. So you will need to create an in-memory DB for each test suite run.

Summary

I tried to highlight the problems that may occur with data access actors when you design your actor system. This article is definitely just the tip of the iceberg, though. Maybe I missed something when I tried to enforce separation of concerns in the context of actors. Anyway, I’ll be really glad to read about your experience in this area.

How would you implement this data access actor?

Download Building Reactive Microservices in Java: Asynchronous and Event-Based Application Design. Brought to you in partnership with Red Hat

Topics:
java ,akka actors ,data access ,postgres ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}