
Enables building concurrent systems using the actor model, leveraging coroutines for asynchronous message passing. Supports clustering for scalability and fault tolerance, under active development.
A small actor system written in kotlin using Coroutines.
🏠 Homepage (under construction)
The actor model is a design paradigm for building concurrent systems where the basic unit of computation, known as an actor, encapsulates its own state and behavior and interacts with others solely through asynchronous message passing. Each actor processes messages sequentially, which simplifies managing state changes and avoids common pitfalls like race conditions and deadlocks that arise with traditional multithreading approaches.
This model is particularly useful for highly concurrent, distributed, and fault-tolerant systems. Its scalability and resilience come from the ability to isolate errors within individual actors through supervision strategies, making it a fitting choice for applications such as real-time data processing, microservices architectures, and any system that requires robust fault isolation and maintainability.
tell (fire-and-forget) for asynchronous communicationask (request-response) for synchronous communication with timeout supportNote: This project is still under heavy development, so you might encounter some incompatibilities along the way.
What’s Next?
Cluster support in the actor4k provides essential capabilities for building robust, scalable, and highly available systems. By enabling actors to seamlessly communicate and coordinate across multiple nodes, clustering significantly enhances fault tolerance, ensures workload distribution efficiency, and maintains stable performance in distributed and high-concurrency environments. This functionality is currently under active development; you can follow its progress and contribute to its advancement through the actor4k-cluster module.
implementation("io.github.smyrgeorge:actor4k:x.y.z")actor4k tries to be multiplatform compatible, which means there is no support for reflection. Therefore, we must pass the factories to the registry for each Actor class in our project (see the example below).
// Create the Logger Factory.
val loggerFactory = SimpleLoggerFactory()
// Create the Actor Registry.
val registry = SimpleActorRegistry(loggerFactory)
.factoryFor(AccountActor::class) { key ->
AccountActor(key) // You can define how an Actor is created.
// You can also pass other arguments to the Actor at this point like, for example,
// AccountActor(key, arg1, ...)
// This can be very helpful with dependency injection scenarios.
}
// Start the actor system.
ActorSystem
.register(loggerFactory)
.register(registry) // You can override the registry implementation here.
.start()class AccountActor(key: String) : Actor<Protocol, Protocol.Response>(key) {
override suspend fun onBeforeActivate() {
// Optional override.
log.info("[${address()}] onBeforeActivate")
}
override suspend fun onActivate(m: Protocol) {
// Optional override.
log.info("[${address()}] onActivate: $m")
}
override suspend fun onReceive(m: Protocol): Behavior<Protocol.Response> {
log.info("[${address()}] onReceive: $m")
return when (m) {
is Protocol.Ping -> Behavior.Reply(Protocol.Pong("Pong!"))
}
}
override suspend fun onShutdown() {
// Optional override.
log.info("[${address()}] onShutdown")
}
sealed interface Protocol : ActorProtocol {
sealed class Message<R : ActorProtocol.Response> : Protocol, ActorProtocol.Message<R>()
sealed class Response : ActorProtocol.Response()
data class Ping(val message: String) : Message<Pong>()
data class Pong(val message: String) : Response()
}
}This example shows a basic actor implementation. If you need an actor that can change its behavior dynamically at runtime (for implementing state machines or context-dependent processing), check out the Behavior Actor section below.
Now let's send some messages:
// [Create/Get] the desired actor from the registry.
val actor: ActorRef = ActorSystem.get(AccountActor::class, "ACC0010")
// [Tell] something to the actor (asynchronous operation).
actor.tell(Protocol.Req(message = "[tell] Hello World!")).getOrThrow()
// [Ask] something to the actor (synchronous operation).
val res = actor.ask(Protocol.Req(message = "[ask] Ping!")).getOrThrow()
println(res)See all the available examples here.
The actor registry is a central component within an actor system that is responsible for managing the lifecycle of actor instances. It maintains a mapping between unique actor addresses and their corresponding instances, ensuring that each actor can be efficiently retrieved and managed. Additionally, the registry stores factory functions for various actor types, which are used to dynamically create new actors when requested. By handling registration, retrieval, and cleanup of actors in a thread-safe manner, the actor registry plays a crucial role in supporting the scalability and reliability of the overall system.
// [Create/Get] the desired actor from the registry.
val actor: ActorRef = ActorSystem.get(AccountActor::class, "ACC0010")A detached actor is an actor instance manually created by directly invoking its constructor, rather than being automatically instantiated and managed by the actor registry. This means that it stands apart from the typical lifecycle management and messaging infrastructure of the actor system until it is explicitly integrated. Detached actors are useful in scenarios where you need fine-grained control over the actor's initialization or when they play a specialized role that doesn't require the full orchestration provided by the system's registry mechanisms.
// [Create] the desired actor.
// We also need to manually [activate] the actor.
val detached = AccountActor("DETACHED").apply { activate() }
detached.tell(Protocol.Req(message = "[ask] Ping!"))
// This actor will never close until we call the shutdown method.
detached.shutdown()Before diving deeper, note that there are several quick actor (detached) builders available:
actorOf – Create and activate a lightweight, stateful actor with lifecycle hooks and typed protocol/responses.simpleActorOf – Convenience wrapper over actorOf that always replies to withSimpleResponse<State>.routerActorOf – Build a RouterActor with a routing strategy and N worker actors.simpleRouterActorOf – Convenience wrapper over routerActorOf that always replies to withSimpleResponse<State>.The actorOf builder provides a lightweight, functional approach to creating actors without needing to define a full
actor class. This is particularly useful for simple actors, prototyping, or when you want to avoid the
overhead of registering actors in the actor registry.
// Define your protocol
sealed interface CounterProtocol : ActorProtocol {
sealed class Message<R : ActorProtocol.Response> : CounterProtocol, ActorProtocol.Message<R>()
sealed class Response : ActorProtocol.Response()
data object GetValue : Message<CurrentValue>()
data object Increment : Message<CurrentValue>()
data object Decrement : Message<CurrentValue>()
data class CurrentValue(val value: Int) : Response()
}
// Create an actor with state
val counter = actorOf<Int, CounterProtocol, CounterProtocol.Response>(initial = 0) { state, message ->
when (message) {
is CounterProtocol.GetValue -> Unit
is CounterProtocol.Increment -> state.value += 1
is CounterProtocol.Decrement -> state.value -= 1
}
Behavior.Reply(CounterProtocol.CurrentValue(state.value))
}This approach is ideal for:
The simpleActorOf helper builds on top of actorOf to make the most common case—single-state update with a simple
response—concise. Instead of defining a full ActorProtocol with typed replies, you can model messages as
SimpleMessage<T> and always receive a SimpleResponse<T> where T is your state type.
// Define lightweight messages
data object GetValue : SimpleMessage<Int>()
data object Increment : SimpleMessage<Int>()
data object Decrement : SimpleMessage<Int>()
// Create a simple stateful actor
val counter = simpleActorOf(initial = 0) { state, message ->
when (message) {
is GetValue -> state // read-only
is Increment -> state + 1 // mutate by returning the new state
is Decrement -> state - 1
else -> state
}
}
// Ask returns SimpleResponse<Int>
val afterInc = counter.ask(Increment).getOrThrow()
println(afterInc.value) // 1The generic actor serves as an abstract foundation for creating actors tailored specifically to your application's
requirements. Defined through the Actor
abstract class, it provides a structured way to implement essential behaviors needed in concurrent environments.
By extending the Actor base class, you gain access to built-in lifecycle hooks such as:
onBeforeActivate: opens a hook prior to activation, ideal for initial configuration or asynchronous
preparations.onActivate: a method triggered upon receiving the first initialization message, enabling state initialization
or custom setup logic.onReceive: a central message handler, mandatory for defining an actor’s response logic.onShutdown: a finalization hook useful for closing resources, saving state, or performing cleanup procedures
while gracefully shutting down.Each actor instance encapsulates its unique state and interacts exclusively through asynchronous message passing, ensuring thread-safe operation and simplifying concurrency management.
The Actor system provides a message stashing mechanism that allows actors to temporarily defer processing of messages that cannot be handled in the current state. This is particularly useful for implementing state-dependent message processing logic.
Key features of message stashing:
stash method to temporarily store the message in a dedicated stash queue.unstashAll() method, which
moves them back to the actor's mailbox for processing.capacity property.stashedMessages counter in its
statistics.Message stashing is particularly useful in scenarios where:
The Actor class includes a capacity
property, which determines the maximum number of messages allowed in an actor's mailbox. By default, this is set to
unlimited. Adjusting this property enables you to control mailbox capacity explicitly. If the mailbox reaches its
maximum capacity, any following attempts to send messages will suspend until space becomes available. Leveraging this
behavior provides an effective way to implement a back-pressure strategy within your actor-based application.
A Router Actor is an actor designed to distribute received messages among multiple worker actors according to a defined routing strategy. This pattern simplifies concurrent and parallel message handling, effectively supporting greater scalability and throughput. The RouterActor provides mechanisms for dynamic management and structured communication patterns with its worker actors, encapsulating common strategies useful in concurrent systems.
The RouterActor extends the foundational Actor abstraction, managing several essential roles:
The Router Actor provides four routing strategies:
ask operation cannot be used with this strategy.The RouterActor implementation includes:
Check an example here.
A Behavior Actor is an actor that can change its behavior dynamically based on the current state or message received. This pattern enables an actor to respond differently to the same message types depending on its current state, making it ideal for implementing state machines or actors that need to adapt their processing logic based on previous interactions.
The BehaviorActor extends the foundational Actor abstraction, providing several powerful capabilities:
become method, enabling state-dependent responses.To use a BehaviorActor, you need to:
become method to change the actor's current behavior based on conditions or messages.The BehaviorActor provides utility methods to simplify behavior management:
become: Changes the actor's current behavior to a new function.The BehaviorActor can be used to implement actors that need to change their behavior based on their state or the messages they receive. For example, an account actor might switch between normal and echo behaviors:
class AccountBehaviourActor(key: String) : BehaviorActor<Protocol, Protocol.Response>(key) {
override suspend fun onActivate(m: Protocol) {
// Set the default behavior here.
become(normal)
}
sealed interface Protocol : ActorProtocol {
sealed class Message<R : ActorProtocol.Response> : Protocol, ActorProtocol.Message<R>()
sealed class Response : ActorProtocol.Response()
data class Ping(val message: String) : Message<Pong>()
data class Pong(val message: String) : Response()
data class SwitchBehavior(val behavior: String) : Message<BehaviorSwitched>()
data class BehaviorSwitched(val message: String) : Response()
}
companion object {
private val normal: suspend (AccountBehaviourActor, Protocol) -> Behavior<Protocol.Response> = { ctx, m ->
ctx.log.info("[${ctx.address()}] normalBehavior: $m")
when (m) {
is Protocol.Ping -> Behavior.Reply(Protocol.Pong("Pong!"))
is Protocol.SwitchBehavior -> {
ctx.become(echo)
Behavior.Reply(Protocol.BehaviorSwitched("Switched to echo behavior"))
}
}
}
private val echo: suspend (AccountBehaviourActor, Protocol) -> Behavior<Protocol.Response> = { ctx, m ->
ctx.log.info("[${ctx.address()}] echoBehavior: $m")
when (m) {
is Protocol.Ping -> Behavior.Reply(Protocol.Pong("Echo: ${m.message}"))
is Protocol.SwitchBehavior -> {
ctx.become(normal)
Behavior.Reply(Protocol.BehaviorSwitched("Switched to normal behavior"))
}
}
}
}
}Check a complete example here.
./gradlew buildYou can also build for specific targets.
./gradlew build -Ptargets=macosArm64,macosX64To build for all available targets, run:
./gradlew build -Ptargets=allA small actor system written in kotlin using Coroutines.
🏠 Homepage (under construction)
The actor model is a design paradigm for building concurrent systems where the basic unit of computation, known as an actor, encapsulates its own state and behavior and interacts with others solely through asynchronous message passing. Each actor processes messages sequentially, which simplifies managing state changes and avoids common pitfalls like race conditions and deadlocks that arise with traditional multithreading approaches.
This model is particularly useful for highly concurrent, distributed, and fault-tolerant systems. Its scalability and resilience come from the ability to isolate errors within individual actors through supervision strategies, making it a fitting choice for applications such as real-time data processing, microservices architectures, and any system that requires robust fault isolation and maintainability.
tell (fire-and-forget) for asynchronous communicationask (request-response) for synchronous communication with timeout supportNote: This project is still under heavy development, so you might encounter some incompatibilities along the way.
What’s Next?
Cluster support in the actor4k provides essential capabilities for building robust, scalable, and highly available systems. By enabling actors to seamlessly communicate and coordinate across multiple nodes, clustering significantly enhances fault tolerance, ensures workload distribution efficiency, and maintains stable performance in distributed and high-concurrency environments. This functionality is currently under active development; you can follow its progress and contribute to its advancement through the actor4k-cluster module.
implementation("io.github.smyrgeorge:actor4k:x.y.z")actor4k tries to be multiplatform compatible, which means there is no support for reflection. Therefore, we must pass the factories to the registry for each Actor class in our project (see the example below).
// Create the Logger Factory.
val loggerFactory = SimpleLoggerFactory()
// Create the Actor Registry.
val registry = SimpleActorRegistry(loggerFactory)
.factoryFor(AccountActor::class) { key ->
AccountActor(key) // You can define how an Actor is created.
// You can also pass other arguments to the Actor at this point like, for example,
// AccountActor(key, arg1, ...)
// This can be very helpful with dependency injection scenarios.
}
// Start the actor system.
ActorSystem
.register(loggerFactory)
.register(registry) // You can override the registry implementation here.
.start()class AccountActor(key: String) : Actor<Protocol, Protocol.Response>(key) {
override suspend fun onBeforeActivate() {
// Optional override.
log.info("[${address()}] onBeforeActivate")
}
override suspend fun onActivate(m: Protocol) {
// Optional override.
log.info("[${address()}] onActivate: $m")
}
override suspend fun onReceive(m: Protocol): Behavior<Protocol.Response> {
log.info("[${address()}] onReceive: $m")
return when (m) {
is Protocol.Ping -> Behavior.Reply(Protocol.Pong("Pong!"))
}
}
override suspend fun onShutdown() {
// Optional override.
log.info("[${address()}] onShutdown")
}
sealed interface Protocol : ActorProtocol {
sealed class Message<R : ActorProtocol.Response> : Protocol, ActorProtocol.Message<R>()
sealed class Response : ActorProtocol.Response()
data class Ping(val message: String) : Message<Pong>()
data class Pong(val message: String) : Response()
}
}This example shows a basic actor implementation. If you need an actor that can change its behavior dynamically at runtime (for implementing state machines or context-dependent processing), check out the Behavior Actor section below.
Now let's send some messages:
// [Create/Get] the desired actor from the registry.
val actor: ActorRef = ActorSystem.get(AccountActor::class, "ACC0010")
// [Tell] something to the actor (asynchronous operation).
actor.tell(Protocol.Req(message = "[tell] Hello World!")).getOrThrow()
// [Ask] something to the actor (synchronous operation).
val res = actor.ask(Protocol.Req(message = "[ask] Ping!")).getOrThrow()
println(res)See all the available examples here.
The actor registry is a central component within an actor system that is responsible for managing the lifecycle of actor instances. It maintains a mapping between unique actor addresses and their corresponding instances, ensuring that each actor can be efficiently retrieved and managed. Additionally, the registry stores factory functions for various actor types, which are used to dynamically create new actors when requested. By handling registration, retrieval, and cleanup of actors in a thread-safe manner, the actor registry plays a crucial role in supporting the scalability and reliability of the overall system.
// [Create/Get] the desired actor from the registry.
val actor: ActorRef = ActorSystem.get(AccountActor::class, "ACC0010")A detached actor is an actor instance manually created by directly invoking its constructor, rather than being automatically instantiated and managed by the actor registry. This means that it stands apart from the typical lifecycle management and messaging infrastructure of the actor system until it is explicitly integrated. Detached actors are useful in scenarios where you need fine-grained control over the actor's initialization or when they play a specialized role that doesn't require the full orchestration provided by the system's registry mechanisms.
// [Create] the desired actor.
// We also need to manually [activate] the actor.
val detached = AccountActor("DETACHED").apply { activate() }
detached.tell(Protocol.Req(message = "[ask] Ping!"))
// This actor will never close until we call the shutdown method.
detached.shutdown()Before diving deeper, note that there are several quick actor (detached) builders available:
actorOf – Create and activate a lightweight, stateful actor with lifecycle hooks and typed protocol/responses.simpleActorOf – Convenience wrapper over actorOf that always replies to withSimpleResponse<State>.routerActorOf – Build a RouterActor with a routing strategy and N worker actors.simpleRouterActorOf – Convenience wrapper over routerActorOf that always replies to withSimpleResponse<State>.The actorOf builder provides a lightweight, functional approach to creating actors without needing to define a full
actor class. This is particularly useful for simple actors, prototyping, or when you want to avoid the
overhead of registering actors in the actor registry.
// Define your protocol
sealed interface CounterProtocol : ActorProtocol {
sealed class Message<R : ActorProtocol.Response> : CounterProtocol, ActorProtocol.Message<R>()
sealed class Response : ActorProtocol.Response()
data object GetValue : Message<CurrentValue>()
data object Increment : Message<CurrentValue>()
data object Decrement : Message<CurrentValue>()
data class CurrentValue(val value: Int) : Response()
}
// Create an actor with state
val counter = actorOf<Int, CounterProtocol, CounterProtocol.Response>(initial = 0) { state, message ->
when (message) {
is CounterProtocol.GetValue -> Unit
is CounterProtocol.Increment -> state.value += 1
is CounterProtocol.Decrement -> state.value -= 1
}
Behavior.Reply(CounterProtocol.CurrentValue(state.value))
}This approach is ideal for:
The simpleActorOf helper builds on top of actorOf to make the most common case—single-state update with a simple
response—concise. Instead of defining a full ActorProtocol with typed replies, you can model messages as
SimpleMessage<T> and always receive a SimpleResponse<T> where T is your state type.
// Define lightweight messages
data object GetValue : SimpleMessage<Int>()
data object Increment : SimpleMessage<Int>()
data object Decrement : SimpleMessage<Int>()
// Create a simple stateful actor
val counter = simpleActorOf(initial = 0) { state, message ->
when (message) {
is GetValue -> state // read-only
is Increment -> state + 1 // mutate by returning the new state
is Decrement -> state - 1
else -> state
}
}
// Ask returns SimpleResponse<Int>
val afterInc = counter.ask(Increment).getOrThrow()
println(afterInc.value) // 1The generic actor serves as an abstract foundation for creating actors tailored specifically to your application's
requirements. Defined through the Actor
abstract class, it provides a structured way to implement essential behaviors needed in concurrent environments.
By extending the Actor base class, you gain access to built-in lifecycle hooks such as:
onBeforeActivate: opens a hook prior to activation, ideal for initial configuration or asynchronous
preparations.onActivate: a method triggered upon receiving the first initialization message, enabling state initialization
or custom setup logic.onReceive: a central message handler, mandatory for defining an actor’s response logic.onShutdown: a finalization hook useful for closing resources, saving state, or performing cleanup procedures
while gracefully shutting down.Each actor instance encapsulates its unique state and interacts exclusively through asynchronous message passing, ensuring thread-safe operation and simplifying concurrency management.
The Actor system provides a message stashing mechanism that allows actors to temporarily defer processing of messages that cannot be handled in the current state. This is particularly useful for implementing state-dependent message processing logic.
Key features of message stashing:
stash method to temporarily store the message in a dedicated stash queue.unstashAll() method, which
moves them back to the actor's mailbox for processing.capacity property.stashedMessages counter in its
statistics.Message stashing is particularly useful in scenarios where:
The Actor class includes a capacity
property, which determines the maximum number of messages allowed in an actor's mailbox. By default, this is set to
unlimited. Adjusting this property enables you to control mailbox capacity explicitly. If the mailbox reaches its
maximum capacity, any following attempts to send messages will suspend until space becomes available. Leveraging this
behavior provides an effective way to implement a back-pressure strategy within your actor-based application.
A Router Actor is an actor designed to distribute received messages among multiple worker actors according to a defined routing strategy. This pattern simplifies concurrent and parallel message handling, effectively supporting greater scalability and throughput. The RouterActor provides mechanisms for dynamic management and structured communication patterns with its worker actors, encapsulating common strategies useful in concurrent systems.
The RouterActor extends the foundational Actor abstraction, managing several essential roles:
The Router Actor provides four routing strategies:
ask operation cannot be used with this strategy.The RouterActor implementation includes:
Check an example here.
A Behavior Actor is an actor that can change its behavior dynamically based on the current state or message received. This pattern enables an actor to respond differently to the same message types depending on its current state, making it ideal for implementing state machines or actors that need to adapt their processing logic based on previous interactions.
The BehaviorActor extends the foundational Actor abstraction, providing several powerful capabilities:
become method, enabling state-dependent responses.To use a BehaviorActor, you need to:
become method to change the actor's current behavior based on conditions or messages.The BehaviorActor provides utility methods to simplify behavior management:
become: Changes the actor's current behavior to a new function.The BehaviorActor can be used to implement actors that need to change their behavior based on their state or the messages they receive. For example, an account actor might switch between normal and echo behaviors:
class AccountBehaviourActor(key: String) : BehaviorActor<Protocol, Protocol.Response>(key) {
override suspend fun onActivate(m: Protocol) {
// Set the default behavior here.
become(normal)
}
sealed interface Protocol : ActorProtocol {
sealed class Message<R : ActorProtocol.Response> : Protocol, ActorProtocol.Message<R>()
sealed class Response : ActorProtocol.Response()
data class Ping(val message: String) : Message<Pong>()
data class Pong(val message: String) : Response()
data class SwitchBehavior(val behavior: String) : Message<BehaviorSwitched>()
data class BehaviorSwitched(val message: String) : Response()
}
companion object {
private val normal: suspend (AccountBehaviourActor, Protocol) -> Behavior<Protocol.Response> = { ctx, m ->
ctx.log.info("[${ctx.address()}] normalBehavior: $m")
when (m) {
is Protocol.Ping -> Behavior.Reply(Protocol.Pong("Pong!"))
is Protocol.SwitchBehavior -> {
ctx.become(echo)
Behavior.Reply(Protocol.BehaviorSwitched("Switched to echo behavior"))
}
}
}
private val echo: suspend (AccountBehaviourActor, Protocol) -> Behavior<Protocol.Response> = { ctx, m ->
ctx.log.info("[${ctx.address()}] echoBehavior: $m")
when (m) {
is Protocol.Ping -> Behavior.Reply(Protocol.Pong("Echo: ${m.message}"))
is Protocol.SwitchBehavior -> {
ctx.become(normal)
Behavior.Reply(Protocol.BehaviorSwitched("Switched to normal behavior"))
}
}
}
}
}Check a complete example here.
./gradlew buildYou can also build for specific targets.
./gradlew build -Ptargets=macosArm64,macosX64To build for all available targets, run:
./gradlew build -Ptargets=all