2

I'm fairly new to both Scala and Akka and I'm trying to figure out how you would create a proper domain model, which also is an Actor.

Let's imagine we have a simple business case where you can open a new Bank Account. Let's say that one of the rules is that you can only create one bank account per last name (not realistic, but just for the sake of simplicity). My first approach, without applying any business rules, would look something like this:

object Main {
  def main(args: Array[String]): Unit = {
    implicit val system = ActorSystem("accout")
    implicit val materializer = ActorMaterializer()
    implicit val executionContext = system.dispatcher
    val account = system.actorOf(Props[Account])
    account ! CreateAccount("Doe")
  }
}

case class CreateAccount(lastName: String)

class Account extends Actor {

  var lastName: String = null

  override def receive: Receive = {
    case createAccount: CreateAccount =>
      this.lastName = lastName
  }
}

Eventually you would persist this data somewhere. However, when adding the rule that there can only be one Bank Account per last name, a query to some data storage needs to be done. Let's say we put that logic inside a repository and the repository eventually returns an Account, we get to the problem where Account isn't an Actor anymore, since the repository won't be able to create Actors.

This is definitely a wrong implementation and not how Actors should be used. My question is, what are ways to solve these kind of problems? I am aware that my knowledge of Akka is not on a decent level yet, so it might be a weird/stupid formulated question.

Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125
Frank Levering
  • 401
  • 3
  • 16
  • 1
    Before digging too deeply into the actor bits, you may want to review the literature on "set validation" in domain models. For instance: http://codebetter.com/gregyoung/2010/08/12/eventual-consistency-and-set-validation/ – VoiceOfUnreason Mar 29 '17 at 18:05
  • Hmm, the link seems to be down. – Frank Levering Mar 29 '17 at 18:10
  • Thanks, this is amazing and helps a lot! – Frank Levering Mar 29 '17 at 18:29
  • Creating a domain model in an actor is actually the wrong approach. Its the application of a one-size-fits-all paradigm when that is not usually the case. For example you might have a user actor that allows the user to change their profile information which is a cluster sharded actor but another actor to deliver real time concurrent data on user activities. PM me on SO and maybe I can help you on a skype call. – Robert Simmons Jr. Apr 03 '17 at 14:43
  • @RobertSimmonsJr. Unfortunately I am not able to send you a PM. Any other way I can contact you? – Frank Levering Apr 03 '17 at 16:19
  • Hmm didn't realize that wasn't possible. Let me write up a formal answer and we can talk here. – Robert Simmons Jr. Apr 03 '17 at 16:30

2 Answers2

7

General Design

Actors should generally be simple dispatchers to business logic and contain as little functionality as possible. Think of Actors as similar to a Future; when you want concurrency in scala you don't extend the Future class, you just use Future functionality around your existing logic.

Limiting your Actors to bare-bones responsibility has several advantages:

  1. Testing the code can be done without having to construct ActorSystems, probes, ActorRefs, etc...
  2. The business logic can easily be transplanted to other asynchronous libraries, e.g. Futures and akka streams.
  3. It's easier to create a "proper domain model" with plain old classes and functions than it is with Actors.
  4. Placing business logic in Actors naturally emphasizes a more object oriented code/system design rather than a functional approach (we picked scala for a reason).

Business Logic (No Akka)

Here we will setup all of the domain specific logic without using any akka related "stuff".

object BusinessLogicDomain {

  type FirstName = String
  type LastName = String 

  type Balance = Double

  val defaultBalance : Balance = 0.0

  case class Account(firstName : FirstName, 
                     lastName : LastName, 
                     balance : Balance = defaultBalance)

Lets model your account directory as a HashMap:

  type AccountDirectory = HashMap[LastName, Account]

  val emptyDirectory : AccountDirectory = HashMap.empty[LastName, Account]

We can now create a function that matches your requirements for distinct account per last name:

  val addAccount : (AccountDirectory, Account) => AccountDirectory =
    (accountDirectory, account) =>
      if(accountDirectory contains account.lastName)
        accountDirectory
      else 
        accountDirectory + (account.lastName -> account)

}//end object BusinessLogicDomain

Repository (Akka)

Now that the unpolluted business code is complete, and isolated, we can add the concurrency layer on top of the foundational logic.

We can use the become functionality of Actors to store the state and respond to requests:

import BusinessLogicDomain.{Account, AccountDirectory, emptyDirectory, addAccount}

case object QueryAccountDirectory

class RepoActor(accountDirectory : AccountDirectory = emptyDirectory) extends Actor {

  val statefulReceive : AccountDirectory => Receive = 
    currentDirectory => {
      case account : Account     => 
        context become statefulReceive(addAccount(currentDirectory, account))
      case QueryAccountDirectory => 
        sender ! currentDirectory
    }      

  override def receive : Receive = statefulReceive(accountDirectory)
}
Ramón J Romero y Vigil
  • 17,373
  • 7
  • 77
  • 125
7

This might be a long answer and I am sorry there isn't a TLDR version. :)

Ok, so you want to "Actorize" your domain model? Bad idea. Domain models are not necessarily actors. Sometimes they are but often they are not. It would be an anti-pattern to deploy one actor per domain model because if you do that you are simply offloading the method calling to message calling but losing all of the single threaded paradigm of the method calling. You cannot guarantee the timing of the messages hitting your actor and programming based upon ASK patterns is a good way to introduce a system that is not scalable, eventually you have too many threads and too many futures and cant proceed further, the system bogs and chokes. So what does that mean for your particular problem?

First you have to stop thinking of the domain model as a single thing and definitely stop using POJO entities. I entirely agree with Martin Fowler when he discusses the anemic domain model. In a well built actor system there will often be three domain models. One is the persisted model which has entities that model your database. The second is the immutable model. This is the model that the actors use to communicate with each other. All the entities are immutable from the bottom up, all collections unmodifiable, all objects only have getters, all constructors copy the collections to new immutable collections. The immutable model means your actors never have to copy anything, they just pass around references to data. Lastly you will have the API model, this is usually the set of entities that model the JSON for the clients to consume. The API model is there to insulate the back end from client code changes and vice versa, its the contract between the systems.

To create your actors stop thinking about your persistent model and what you will do with it but instead start thinking of the use cases. What does your system have to do? Model your actors based on the use cases and that will change the implementation of the actors and their deployment strategies.

For example, consider a server that delivers inventory information to users including current stock levels, reviews by users and so on for products by a single vendor. The users hammer this information and it changes quickly as stock levels change. This information is likely stored in half a dozen different tables. We don't model an actor for each table but rather a single actor to serve this use case. In this case this information is accessed by a large group of people in heavy load environment. So we are best creating an actor to aggregate all of this data and replicating the actor to each node and whenever the data changes we inform all replicants on all nodes of the changes. This means the user getting the overview doesn't even touch the database. They hit the actors, get the immutable model, convert that to the API model and then return the data.

On the other hand if a user wants to change the stock levels, we need to make sure that two users don't do it concurrently yet large DB transactions slows down the system massively. So instead we pick one node that will hold the stock management actor for that vendor and we cluster shard the actor. Any requests are routed to that actor and handled serially. The company user logs in and notes the receipt of a delivery of 20 new items. The message goes from whatever node they hit to the node holding the actor for that vendor, the vendor then makes the appropriate database changes and the broadcasts the change which is picked up by all the replicated inventory view actors to change their data.

Now this is simplistic because you have to deal with lost messages (read the articles on why reliable messaging is not necessary). However once you start to go down that road you soon realize that simply making your domain model an actor system is an anti-pattern and there are better ways to do things.

Anyway that is my 2 cents :)

Robert Simmons Jr.
  • 1,182
  • 8
  • 21
  • It starts to make a lot more sense now, also the use case of when to use the Actor Model. However, the use case you are providing states that there is some sort of data that a lot of users want to retrieve. So Actors across all the nodes hold the same state and is updated whenever necessary. But how would you solve f.e. withdrawing money from a bank account of a specific user? Would you send a message to an Actor with some sort of ID's, retrieve the specific bank account from a datastore and update the balance? Or wouldn't you even use Actors for such a thing? – Frank Levering Apr 03 '17 at 17:24
  • Absolutely actors. The case your are talking about is a serial one. But it can be modeled simply. Write one transaction to the DB containing everything. Then process that transaction with actors. In this case your UserActor is probably cluster sharded (see akka docs) and so is the bank. In our case our UserActor also used become() to stash other messages until this one was done. Its a false premise to think you need ACID imperative processing for a bank. All you need is an atomic transaction where you either write the wallet balance and the transaction or nothing at all. – Robert Simmons Jr. Apr 03 '17 at 17:32
  • Good to know I'm not on the wrong path here. The way I tried to solve this specific problem (I am just writing a bank account functionality for fun to get more familiar with Scala and Akka) is to have a BankAccount Actor that receives a message in the form of AddTransaction. The Actor eventually will persist an event which get caught by a Persistence Query which on his turn updates the read side (i.e. the balance). Is there anything wrong with this approach, taking in mind I need to deal with eventual consistency? – Frank Levering Apr 03 '17 at 17:53
  • The balance is a facet of the rollups of the transaction. If you can guarantee the transaction was written you can alter the balance in the immutable data of the actor. Send the AccountActor a withdraw message with amount. It writes the transaction and the balance change atomically. This is a trivial example. An example where money goes between accounts is better. Just as before except the transaction contains both account ids. The transaction is written by one and the other is informed of the change and updates its state. – Robert Simmons Jr. Apr 03 '17 at 18:12
  • Purchases are a much more interesting example because there is an inventory actor involved but all you have to do is remember that in real life people overdraw their account and then all should fall into place immediately. Real life backing is eventually consistent, not immediately – Robert Simmons Jr. Apr 03 '17 at 18:13