Distributed Domain-Driven Design in Akka Actor System

Reading Time: 5 minutes

Domain-Driven Design is a set of guiding principles for software architecture. The most important concept in DDD is the focus on the Domain Model. If you are not familiar with the term “domain,” it is the set of requirements, constraints, and concepts that make up the business or field that you are trying to model.

In Domain-Driven Design, we focus on that business domain and we model our software around it. We are trying to model our software in a way that reflects the real world and how it operates. We want our model to capture the truth of the domain that we are trying to model. This involves conversations with domain experts. These experts are people who are knowledgeable about the domain but might not be technical.

Components of Domain-Driven Design

The goal with DDD is to decompose the larger domain into smaller, easier-to-manage chunks. You can then model those chunks individually, and in doing so, you can develop a better understanding not just for yourself, but for the domain experts, as well.

DDD provides a set of tools that you can apply directly to the code. These are your building
blocks.

Domain Entities

Domain-Driven Design uses the concept of an Entity to refer to objects in the system that are uniquely
identifiable by a key or composite key.

The nature of Entities, the fact that they can contain mutable states and are uniquely identifiable, maps directly to Akka actors. Actors are all about managing mutable states. And every actor in the system is uniquely identifiable using its path, regardless of the data that the actor contains. It is therefore natural for us to use actors in our system the same way we might use Entities in a nonactor-based system. You can treat them as equivalent. For instance, if your system has a user Entity, you could model that user as an actor:

class User(id: UUID) extends Actor{
   override def receive: Receive = ...
}

When our User actor receives messages, the actor’s internal state may change. But the path to that actor and the ID of the User doesn’t change. Those are fixed values. This means that this actor is always uniquely identifiable either by the path or by the ID. This makes the User an Entity.

Domain Value Objects

Value Objects, on the other hand, are different from Entities. Two Value Objects that contain the same data are considered to be equal; you don’t bother trying to distinguish between them. Value Objects, unlike Entities, are immutable.

In Akka, the messages passed between actors are natural Value Objects. Those messages are
immutable if we are following best practices and are usually not identifiable. They are just
data containers.

object User{
  case class SetName(firstName: String, lastName: String)
}

In this case, the SetName message is a Value Object. If you have two SetName objects for which the value of firstName and lastName is the same, those two messages can be considered equivalent.

Aggregates and Aggregate Roots

  • Aggregates are collections of objects within an application. An aggregate creates a logical grouping of many different elements of a system.
  • Every aggregate is bound to an aggregate root.
  • If you want to gain access to some element inside the aggregate, you must go through the aggregate root; you do not access the inner element directly
  • For instance, if your person aggregate root has an address entity, you don’t directly access the address, but instead access the appropriate person, and then reference the contained address.

In Akka, aggregate roots are often represented by parent actors. When you delete/stop those parents, all of their children go with them. But they don’t need to be top-level actors. Sometimes, it is beneficial to have a layer or two between the top level and the actual aggregate roots. For example, if your users are the aggregate roots, you might want a layer above the user that will be the parent of all users.

Distributed Domain-Driven Design in Akka Actor System

For instance, In a scheduling system, we probably have people that need to be scheduled for a particular task. We will represent a person using a Person actor. But that Person might also have a Schedule. It might be desirable (especially when using the Actor Model) to represent that Schedule by using an actor, as well, as demonstrated here:

object Schedule {
  def props = Props(new Schedule)
}

class Schedule extends Actor{
...
}

class Person(id: UUID) extends Actor {
   private val schedule = createSchedule()
   
   protected def createSchedule() = context.actorOf(Schedule.props)
}

You can see in this example that the Person actor definitely aggregates the Schedule. You can’t access the Schedule without going through the Person, and if you delete the Person the Schedule goes with it. This makes Person a candidate for an aggregate.

Repositories

Repositories are where we begin to abstract away our infrastructure concerns. It is use to create an abstraction layer over the top of our storage concerns. The basic approach when working with aggregates in DDD is to go to a repository, retrieve an aggregate from that repository, perform some operation, and then save the aggregate again. This sounds a lot like a database.

  • Repository can be an abstraction over a database.
  • Although a repository could be accessing a database, it could also be pulling data from memory, or from disk, or from the web.
  • The key to using repositories is to understand that they are abstraction layers. For this reason, they are often represented as a trait in Scala.

In Akka, when using the Actor Model, repositories can be a little tricky. The general flow of a repository involves something resembling an Akka ask. You ask the repository for an instance of some aggregate, you perform an operation on that aggregate, and then you instruct the repository to commit that change.

Domain Services

In Akka, services can take many forms. They could be long-lived actors that operate against other aggregate actors. Or, they could be temporary actors that are created to perform some task and then terminated when the task is completed.

An example of a service in the scheduling domain example would be a worker actor that you create to fulfill a particular task. For example, you might want a temporary worker actor to
handle a single request:

class ScheduleRequestService(request: ScheduleRequest) extends Actor {
...
}

The job of the ScheduleRequestService is to manage any state involved with that particular request and to communicate with whatever aggregates are needed during the fulfillment of the request. After the request has been processed completely, the ScheduleRequestService can be terminated.

Conclusion

Overall, Domain-Driven Design can provide a means to provide the structure a system needs. Applications built without such a structure tend to be much more difficult to understand and maintain, and as a result, they lack overall quality. This is especially true with Akka systems built with the Actor Model because the desirable high level of isolation can make it difficult to see the overall design without DDD.

References

https://www.infoq.com/articles/Reactive-Systems-Akka-Actors-DomainDrivenDesign/

knoldus

Written by 

Gulshan Singh is a Software Consultant at Knoldus Inc. Software Developers are forever students. He is passionate about Programming.