You use 5 components, definitely. There are actors dealing with specific tasks, and there's an orchestrator as well.
The question you must have, of course, is how do you chain this assynchronously. Well, it's actually somewhat easy, but it can obscure the code. Basically, you send each componenent the reply you want.
react {
case DeleteTrades(user,dates) =>
PermissionService ! FindPermissions(user, DeleteTradesPermissions(dates) _)
case DeleteTradesPermissions(dates)(ps) =>
if (ps hasPermission "delete")
Persistence ! FindTrades(date, DeleteTradesTradeSet _)
case DeleteTradesTradeSet(ts) =>
ReportService ! SendCancelReports(ts)
PositionService ! UpdateWithDeletedTrades(ts)
}
Here we use currying to pass "dates" in the first returning answer. If there's a lot of parameters associated with an interaction, it might be better to keep the information for all on-going transactions in a local HashSet, and just pass a token that you'll use to locate that information when receiving the answer.
Note that this single actor can handle multiple concurrent actions. In this particular case, just Delete transactions, but you could add any number of different actions for it to handle. When the data needed for one action is ready, then that action continues.
EDIT
Here's a working example of how these classes can be defined:
class Date
class User
class PermissionSet
abstract class Message
case class DeleteTradesPermission(date: Date)(ps: PermissionSet) extends Message
case class FindPermissions(u: User, r: (PermissionSet) => Message) extends Message
FindPermissions(new User, DeleteTradesPermission(new Date) _)
A few explanations on currying and functions. The class DeleteTradesPermission
is curried so that you can pass a Date
on it, and have some other function complete it with a PermissionSet
. This would be the pattern of the answer messages.
Now, the class FindPermissions
receives as a second parameter a function. The actor receiving this message will pass the return value to this function, and will receive a Message
to be sent as answer. In this example, the message will have both the Date
, which the calling actor sent, and PermissionSet
, which the answering actor is providing.
If no answer is expected, such as the case of DeleteTrades
, SendCancelReports
and UpdateWithDeletedTrades
for the purposes of this example, then you don't need to pass a function of the returning message.
Since we are expecting a function which returns a Message as parameter for those messages requiring an answer, we could define traits like this:
trait MessageResponse1[-T1] extends Function1[T1, Message]
trait MessageResponse2[-T1, -T2] extends Function2[T1, T2, Message]
...