I've been working on Cluster Virtual Actors, a plugin built on top of Swift Distributed Actors. It brings the Orleans-style virtual actor model to Swift. If you're familiar with Akka, the closest equivalent is a sharded entity managed through Cluster Sharding.
Each entity—such as user ktoso, order 42, chat room Swift, or ticket FC Bayern Munich vs. Manchester United, row 62, seat 25—is represented by an actor with a stable identity. The cluster handles activation, placement, routing, and idle deactivation, so applications don't need to maintain actor lifecycle maps manually.
The @VirtualActor macro generates the required conformance and spawning boilerplate:
@VirtualActor
distributed actor ChatRoom {
typealias ActorSystem = ClusterSystem
struct Dependency: Codable, Sendable {
let roomID: String
}
private let roomID: String
private var messages: [String] = []
init(actorSystem: ClusterSystem, dependency: Dependency) {
self.actorSystem = actorSystem
self.roomID = dependency.roomID
}
distributed func post(_ message: String) {
messages.append(message)
}
}
Create a cluster system and a VirtualNode:
let (system, virtualNode) = await ClusterSystem.startVirtualNode(
named: "my-node"
) {
$0.autoLeaderElection = .lowestReachable(minNumberOfMembers: 1)
$0.plugins.install(plugin: ClusterSingletonPlugin())
$0.plugins.install(plugin: ClusterVirtualActorsPlugin())
}
// Join the cluster here.
VirtualNodes provide the runtime capacity for virtual actors. One is enough for development, while a production cluster will usually run several. This spreads actors across the cluster and lets the system continue creating them when a node becomes unavailable. Services that only call virtual actors do not need to host a VirtualNode.
You can then resolve an actor by ID from anywhere in the system:
let room: ChatRoom = try await system.virtualActors.getActor(
identifiedBy: VirtualActorID(rawValue: "room-swift"),
dependency: ChatRoom.Dependency(roomID: "swift")
)
try await room.post("Hello!")
Dependency contains the information needed when a new actor instance is created. Here, it carries the application's room ID. It could also include tenant information or other configuration the room needs at startup. If an actor needs no activation context, you can omit Dependency; the macro generates a None value to use at lookup instead.
No ChatRoom needs to be created ahead of time. Looking up room-swift activates it on demand, and concurrent lookups resolve to the same logical actor. Idle instances can be reclaimed and recreated later without callers managing their lifecycle.
The messages in this example live only in memory and disappear after deactivation. Combining virtual actors with event sourcing gives them durable state: an in-memory activation can come and go, while its identity and journal survive. When the actor is needed again, the cluster reactivates it and rebuilds its state from the journal. It behaves like an "always alive" actor without remaining in memory all the time.
Project visible link: GitHub - akbashev/cluster-virtual-actors: Virtual actors for Swift's Cluster Systems · GitHub
Feedback is appreciated! ![]()