DEV Community

Grady Zhuo
Grady Zhuo

Posted on

swift-kurrentdb 2.0.0 — Event Sourcing for Swift, Rebuilt from the Ground Up

I'm excited to announce swift-kurrentdb 2.0.0 — a complete redesign of the Swift client for KurrentDB (formerly EventStoreDB).

If you're building Event Sourcing systems with Server-Side Swift, this release is for you.


Background

KurrentDB is a purpose-built event store for Event Sourcing. It stores events as an append-only log, supports projections, persistent subscriptions, and cluster deployments with leader election.

swift-kurrentdb is the community Swift client for it, built on top of grpc-swift 2.x and Swift Concurrency.

Version 1.x worked, but it wasn't designed with Swift 6's strict concurrency model in mind. Rather than patch it, 2.0 is a ground-up redesign that feels native to modern Swift.


What changed in 2.0

Target-based hierarchical API

The biggest change is how you interact with the client. The old flat-method style is replaced by a composable, target-based design:

// 1.x — flat methods on the client
try await client.appendToStream("orders", events: [event]) {
    $0.revision(expected: .any)
}

// 2.x — select a target, then call the operation
try await client.streams(of: .specified("orders"))
    .append(events: [event]) {
        $0.expectedRevision = .any
    }
Enter fullscreen mode Exit fullscreen mode

The same pattern applies everywhere:

// Streams
client.streams(of: .specified("orders"))
client.streams(of: .all)

// Projections
client.projections(name: "order-count")
client.projections(system: .byCategory)

// Persistent Subscriptions
client.persistentSubscriptions(stream: "orders", group: "workers")
Enter fullscreen mode Exit fullscreen mode

This makes the API more discoverable, consistent, and easier to extend.


Swift 6 strict concurrency — zero data races

KurrentDBClient is an actor. All public operations are async. The entire codebase compiles cleanly under -strict-concurrency=complete — no @unchecked Sendable shortcuts, no suppression pragmas.


Typed throws

Every operation throws KurrentError — not any Error. You always know exactly what can go wrong:

do {
    try await client.streams(of: .specified("orders"))
        .append(events: [event]) {
            $0.expectedRevision = .streamExists
        }
} catch KurrentError.wrongExpectedVersion {
    // optimistic concurrency conflict — handle gracefully
} catch KurrentError.streamNotFound {
    // stream doesn't exist yet
}
Enter fullscreen mode Exit fullscreen mode

No more as? SomeError casting in catch blocks.


Cluster resilience

2.0 adds automatic node re-discovery when a cluster node fails. The client:

  1. Detects the failure via gRPC status codes
  2. Invalidates the cached node
  3. Re-runs gossip-based discovery to find a healthy node
  4. Retries the operation transparently

Retry policy and node cache TTL are now fully configurable:

let settings = ClientSettings.localhost()
    .retryPolicy(.init(maxAttempts: 5, interval: .seconds(2)))
    .nodeCacheTTL(.seconds(30))
Enter fullscreen mode Exit fullscreen mode

Subscription dropped events

Persistent subscriptions now surface a subscriptionDropped error with lastRevision and lastPosition — the exact checkpoint to resume from after an unexpected disconnect:

do {
    for try await result in subscription.events {
        try await subscription.ack(readEvents: result.event)
    }
} catch KurrentError.subscriptionDropped(let reason, let lastRevision, _) {
    // resume from lastRevision on reconnect
}
Enter fullscreen mode Exit fullscreen mode

Migrating from 1.x

I know breaking changes are painful. That's why 2.0 ships a KurrentDB_V1 compatibility library in the same package.

Switch your dependency target:

// Package.swift
.product(name: "KurrentDB_V1", package: "swift-kurrentdb")
Enter fullscreen mode Exit fullscreen mode

Change your import:

import KurrentDB_V1   // was: import KurrentDB
Enter fullscreen mode Exit fullscreen mode

That's it. Your existing 1.x code compiles and runs unchanged. All 1.x methods are marked @deprecated, so Xcode guides you through the migration at your own pace — no big-bang rewrites required.

👉 Full Migration Guide


Quality & compatibility

Tests 174 across 11 suites
Coverage 75% line coverage
Test environment Live 3-node TLS KurrentDB cluster
Server support KurrentDB 25.1, 26.0 · EventStoreDB 24.x
Swift versions 6.0, 6.1, 6.2
Platforms macOS 15+ · iOS 18+ · Linux

Test suites cover streams (append, read, subscribe, optimistic concurrency), projections, persistent subscriptions (ACK/NACK, park, replay), users, operations, gossip, and monitoring — all running against a real cluster, not mocks.


Get started

// Package.swift
.package(url: "<https://github.com/gradyzhuo/swift-kurrentdb.git>", from: "2.0.0")
Enter fullscreen mode Exit fullscreen mode
import KurrentDB

let client = KurrentDBClient(
    settings: .localhost()
        .authenticated(.credentials(username: "admin", password: "changeit"))
)

// Append an event
let event = EventData(eventType: "OrderPlaced", model: Order(id: "123", total: 99.99))
try await client.streams(of: .specified("orders")).append(events: [event])

// Read events
let stream = try await client.streams(of: .specified("orders")).read {
    $0.startFrom(revision: .start).limit(10)
}
for try await response in stream {
    print(try response.event?.record.eventType ?? "checkpoint")
}
Enter fullscreen mode Exit fullscreen mode

Links

Feedback, issues, and PRs are very welcome. Event Sourcing in the Swift ecosystem is still young — let's build it together.

Top comments (0)