Virtual actors for Swift

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! :slightly_smiling_face:

9 Likes

Thank you for sharing.

Do you have a small, working example which I can play with? :slight_smile:

2 Likes

This repo has some basic demo distributed-actors-showcase/let-it-crash at main · akbashev/distributed-actors-showcase · GitHub.

It's a bit more than just showing virtual actors, but worth checking, especially the standalone vs. different ports case and crashing workers (dividing by zero).

2 Likes

Very interesting programming model to use a single actor per entity ID! Trying to wrap my head around persistence with event sourcing. How would I run a query across multiple entities like with a traditional database?

Traditionally in such systems you'd do the Command Query Responsibility Segregation - Wikipedia pattern, and have a separate query side or projections that aggregate/prepare the information you're querying for. It can be surprising to get used to but the way to think about it.

Generally I'd think about virtual actors more like a hot cache with replay and reload rather than a database in the usual sense.

1 Like

Not sure I understand CQRS. :sweat_smile: So would you create another actor that aggregates stuff and call it from the first actor?

Also, does event sourcing mean the event journal keeps growing indefinitely? Sounds like a big enough cost that event sourcing should be used only when needed. Do you have examples of good use cases?

Event sourcing used to be all the rage a while back; the cost of replay isn't growing infinitely IF you perform periodic snapshots; then a replay resumes from a snapshot, and e.g. 100 events on top of it. You may choose to remove old events or not....

The log being the whole history from beginning of time is a feature by itself -- not just a cost. Imagine being able to apply some qualification or decision engine over the history of all transactions someone ever made. You may this way evaluate "would the new system have worked better" etc. It also is an automatic audit trail which may be required in certain compliance situations...

Anyway, if you don't feel you need it and you're happy with CRUD you may be right, it's not for everyone, but it definitely has some neat applications :slight_smile:

1 Like

Well, this is the distributed part of the language, and I’d argue that storage is cheap compared to the cost of building reliable and scalable distributed systems. Append-only logs is simple but very powerful tool for that purpose. There’s a great article by Martin Kleppmann where he discusses logs⁠; the topic later became part of his book.


Would agree with Konrad that it takes time to get used to, but it’s simple enough and powerful tool.