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.

There’s a great article by Martin Kleppmann where he discusses logs

Good read! Brings back memories. When I was building distributed systems, I also used the change data capture approach that Martin describes at the end of the article: write synchronously to a database, the database writes to a change log, and then multiple consumers of the log can process the change.

I’d argue that storage is cheap compared to the cost of building reliable and scalable distributed systems

Well, all the logs that Martin mentioned in the article (database write-ahead log, replication log, Kafka log) have some sort of retention policy to reduce storage cost. I’d argue that an infinitely-growing log should be the exception rather than the default, with audit trail being the main use case. Do you plan to add some ability to delete old events after a successful snapshot?

Speaking more generally, it seems like the paradigm shift with event sourcing is about making the database implicit instead of explicit. What I mean is… event sourcing sort of looks like a database: the event log is like a database’s write-ahead log and the state snapshot is like the tables of the database. So instead of building the database schema by hand and reading/writing to a database explicitly, you just read/write to the State struct and the data is persisted automatically.

But the trade-off with event sourcing is that it is missing the other features of the database like querying across entities, indexes, etc. So it seems like event sourcing is mostly suitable for use cases like audit trail?

By the way, I’m not trying to trash your library! I’m just trying to figure out how I can use it in my own projects.

1 Like

As @ktoso already mentioned in theory you can write a journal with a policy to delete logs after snapshotting. Though I think difference with database is that:

  1. with event sourcing, the event log is the source of truth, while the actor state/snapshot is just a materialised representation of that log;
  2. events are application domain level (e.g. UserEnteredChat) rather implementation detail of the storage engine, and have it's own meaning.

So I would also argue that the whole history is a feature worth paying for.

It doesn’t mean everything should be built on event sourcing though, and you can easily combine it with traditional databases. That's the whole point of libraries—it's quite easy to make distributed actors @EventSourced. :upside_down_face:


And don't worry, it's actually good that some discussions are happening—while virtual actors and event sourcing are a foundation, there is more to build for sure. For example, it probably would be also nice to port the Grain persistence concept at some point, basically saving state on change. Would cover case where storage is limited (or history is irrelevant?).

2 Likes