Actor-oriented programming is a form of object-oriented programming that adds scalability and availability to monolithic applications. It provides the basis for creating milliservices and makes it possible create and invoke virtual actors as if they were just regular local objects.
Object-oriented programming
In traditional object-oriented programming, objects have references to other objects on which they can invoke methods to perform certain actions. For example, a ShoppingCart object can have a reference to a CreditCardValidation object on which it can invoke the Validate method to perform credit card validation.
In combination with structured programming, which also provides control structures and structured exception handing, it is very easy to write decent object-oriented computer programs.
The microservice premium
The downsides: scalability and availability. Traditional computer programs (nowadays also called monoliths – the opposite of the popular microservice) scale very well until they reach the physical limits of memory and CPU. When the load on a monolith becomes more than one processor core can handle, or when it requires more memory than physically available in a computer, it cannot scale anymore. Not naturally, at least.
And that is where the microservice premium kicks in: The costs of refactoring a monolith so that it becomes scalable (and with that, also provides availability) are significant. It is far from trivial to implement multithreading and start using multiple CPU cores. And it is even less trivial to have multiple application instances take over work from each other when one of them stops. Properly doing so without loss of work or duplicate processing of work items is a very complicated matter.
Actor-oriented programming
The main issue with scaling up monolithic applications is that business logic is coupled. Objects assume that the other objects they need live within the same process. That assumption makes it very convenient to program monoliths, but it hinders scalability.
Another issue that monoliths have for scaling up is that state is not concentrated on one or a few central places, but distributed all over the software. Every object has a little bit of state, which makes it difficult to persist state (that is, to be able to store state to disk, a database or the cloud; and to retrieve it later on and get the application back in a consistent state). As we have argued in our article about the milliservice architecture, the capability to persist state, together with spatial decoupling of business logic, it what facilitates scalability and availability.
The actor-oriented programming paradigm extends (or, restricts) the object-oriented programming paradigm by
- Introducing the concept of actors; and
- Requiring that actors only use portals to obtain proxies (stubs) to other actors.
Actors
Actors are in essence just objects. Like regular objects, they have methods that perform actions. And they have state.
The underlying idea is that actors represent business objects. They represent objects that also play a role in the business domain of the application. In fact, they are a way to achieve the digital twin concept. Examples of actors are a shopping cart, a product or a user account.
The fact that actors represent business objects, does not means that every object in your application should be an actor. Lower level functionality can very well be implemented as regular objects.
The key thing is that an actor has the sole and full responsibility for a certain business object. So, there is only one actor that owns a certain shopping cart, and that performs actions like adding a product and doing a check out.
Actors can invoke regular objects and other actor to perform their tasks, but they are the one that is responsible for the business object they do it for, and they should be the only one that maintains the state of that business object.
Portals and proxies
A portal is for actor-oriented programming what a factory is for object-oriented programming. In actor-oriented programming, it is a portal that creates a proxy (stub) object for a local or remote actor, and it is the application that runs actors of a certain type that instantiates the actors for that type.
In well-designed object-oriented programs, object instances are created by the dependency injection root and passed to objects that need these object via injection (typically constructor injection). In actor-oriented programs, actor instances are created by the (possibly remote) application that can run these actors, and objects that use these actors receive a proxy to these remote actors. These proxies are typically created by the dependency injection logic (via a portal), and then passed via constructor injection to the object that uses the remote actor.
The key characteristic of a proxy is that its interface is the same as the interface of the local or remote actor is a proxy to. When the CreditCardActor has a Validate action method, then CreditCardActor stub object must also have a Validate method with the same arguments. Many modern languages nowadays support the async-await pattern, which makes it trivial to invoke such a remote actor method as if it were a regular, local method.
Persistence
In order to achieve scalability and availability, actors must be able to persist their state. It is important that actors can do this without having to know the details of the underlying storage. Actors should focus on business logic, and not be bothered with such implementation details. The principles of clean architecture apply to actors like they do to regular objects.
A framework that facilitates running of actors therefore should also provide a persistence interface that can be used by actors to persist their state. A well-designed framework abstracts away the differences between and particularities of various underlying storage mechanisms (like on disk, in databases, in the cloud) and lets actor just focus on their business logic.
For actors, it is good to become state-aware. With that we mean that they should not just store their internal state scattered around in the application, but try to concentrate their internal state in one or a few places, so that it is easy to persist the state, and to load it back in.
Darlean
Darlean facilitates the actor-oriented programming paradigm for creating scalable milliservices that combine high availability with the ease of programming and deploying that monolithic applications offer. It provides libraries that can be used to write actor-oriented programs, and a runtime that provides a message bus, actor discovery, persistence and more.