Skip to content

Where the clients differ

One kernel contract does not make the client APIs interchangeable. This reference covers appending and concurrency, compliance, reactor delivery, and event seeding. It does not claim parity for other features.

The comparisons below describe the client source inspected on September 23, 2026. The TypeScript rows were checked again on September 30, 2026 against Chronicle.TypeScript v6.31.2. Java uses Java-callable interfaces, blocking facades, and bridges that delegate to the Kotlin implementation. Source links identify the implementation behind each comparison. These are source-level comparisons, not cross-client runtime conformance results.

ClientAppend without an explicit concurrency scope
.NETThe default OptimisticConcurrencyStrategy reads the event source’s tail within the selected stream scope and sends an expected sequence number. ConcurrencyOptions.DefaultStrategy can change this behavior.
Kotlin and JavaUse ConcurrencyScope.none; no optimistic concurrency check is requested.
TypeScriptSends an unset expected sequence number; no optimistic concurrency check is requested.
ElixirSends no concurrency scope.

The .NET default detects an intervening append between its tail read and write. It does not protect an earlier application read automatically. When no event matches the scope, the default leaves the first append unchecked; CheckFirstAppendIntoAScope enables that check and defaults to false.

Choose an explicit scope when correctness depends on the state you previously read. Do not infer the application’s concurrency guarantee from a successful plain append.

Sources: .NET strategy, .NET defaults, JVM append, TypeScript append, Elixir append.

.NET exposes ConcurrencyScopeBuilder.ExpectingNoMatchingEvent; the JVM builder exposes withExpectsNoMatchingEvent(), which its Java bridge documents as directly callable from Java. Both send the kernel’s explicit no-matching-event condition. The TypeScript client sends it too, when the scope’s sequence number is EventSequenceNumber.beforeFirst.

The Elixir client scope converter does not send that condition. An unset expected sequence number is not a substitute: it disables the sequence-number check rather than asserting that no event exists.

Sources: .NET conversion, JVM builder, kernel validation, and the TypeScript and Elixir append implementations linked above.

ClientResult shape
.NETAppendResult exposes the appended position and rejection details. AppendManyResult carries the batch’s sequence numbers and violations.
Kotlin and JavaAppendResult per event; a batch returns a list. Java can read the position through getSequenceNumberValue().
TypeScriptAppendResult per event; a batch returns an array.
Elixir:ok or an error tuple for the ordinary append APIs, without a returned appended position.

Inspect the result for kernel constraint and concurrency rejections. This does not mean append APIs never throw: unknown event types, connection failures, and other client or transport failures can still throw or reject. Kotlin also throws ChronicleCommandRejected for command-level authorization and exception responses.

Sources: .NET results, JVM result, TypeScript result, Elixir result conversion.

ClientWhen no explicit append subject is supplied
.NETResolves an event’s [Subject] property before appending. The kernel falls back to the event source id when no subject is set.
Kotlin and JavaDefault to the event source id. The append path does not resolve event-side @Subject metadata.
TypeScriptUses options.subject ?? eventSourceId; the append path does not resolve an event’s @subject property.
ElixirSends an empty subject, so the kernel falls back to the event source id.

Read-model subject metadata and append subject resolution are separate capabilities. When the person whose PII you store differs from the event source, pass the subject explicitly rather than assuming an annotation selects it in every client. Choosing the wrong subject changes whose erasure key protects the data.

Sources: .NET subject resolver, kernel append, and each client’s append implementation linked above.

All five language surfaces expose subject-key deletion. Only .NET and TypeScript expose the kernel operation that permits a new encryption key afterward: AllowNewEncryptionKeyFor and allowNewEncryptionKeyFor, respectively.

Kotlin, Java, and Elixir do not expose that operation through their client compliance APIs. Workflows that need it must use another supported administrative route or client; deleting a key does not itself authorize a replacement.

Sources: .NET PII API, TypeScript PII API, JVM compliance API, Java bridges, Elixir compliance.

ClientRegistration and handler behavior
.NETReactors are replayable unless the class has [OnceOnly]. Method-level [OnceOnly] skips that handler during replay; [Replay] selects a replay handler.
Kotlin and JavaKotlin registration and dispatch use @OnceOnly and @Replay; Java reactors go through the same JVM registration.
TypeScriptMethod-level @onceOnly() skips that handler during replay; @replay() selects a replay handler. Class-level @onceOnly() sets IsReplayable: false at registration, but the proto3 encoder leaves a false value out, so the kernel registers the reactor as replayable and still replays it. The kernel honors class-level @onceOnly() only once the client also sends IsNotReplayable: true, tracked in Chronicle.TypeScript#236.
ElixirReactor registration sets IsReplayable: true. There is no once-only marker. Per-event context has no replay flag, but optional replay-begin/end and partition-replay callbacks report lifecycle transitions.

These differences follow from registration and dispatch code, together with the kernel’s replay guards. Do not treat an available lifecycle callback as proof that the client’s registered reactors are replayable.

Replay exclusion is not exactly-once delivery. Recovering a failed partition can deliver an event again as an ordinary observation. A side effect still needs idempotency appropriate to the external system.

Sources: .NET registration, JVM registration, JVM handlers, TypeScript reactors, Elixir handlers, kernel replay guards.

.NET supplies a ReactorDelivery handler parameter with a stable Id (of type DeliveryId) suitable for an application’s receipt key. Kotlin, Java, TypeScript, and Elixir have no equivalent client-provided delivery identity.

Outside .NET, define an idempotency key that distinguishes the event store, namespace, event sequence, reactor, and event position; do not use a sequence number alone across stores or sequences. A key identifies the delivery—it does not make the external effect atomic with recording a receipt.

Sources: .NET delivery identity, JVM event context, TypeScript event context, and the Elixir handler implementation linked above.

ClientDefault target and duplicate handlingSeeder callback failure
.NETFor/ForEventSource entries are global. The kernel applies them to namespaces returned by its namespace inventory at seeding time, tracking entry occurrences by source, event type, content, and tags.A callback exception propagates. Activation failures are logged and skipped separately.
TypeScriptfor/forEventSource entries use the kernel’s global seeding path and entry tracking.Logged as a warning and discovery continues; entries added before the failure are not removed.
Kotlin and JavaEntries target the selected namespace, defaulting to the client’s namespace. Uses kernel entry tracking, but does not send seed tags.A callback exception propagates.
ElixirGlobal entries and entries without an explicit for_namespace target the builder’s namespace. Appends directly and skips a source that already contains any events, rather than using kernel entry tracking.Discovery logs and skips the failing seeder, discarding that seeder’s partial entries.

Adding another seed to an existing source can therefore add data through the kernel seeding path but remain unapplied in Elixir. A seed callback completing is also not the same as successful registration; check the owning client’s registration outcome separately.

This comparison does not establish when a namespace created later receives previously registered global seeds.

Sources: .NET seeding, kernel seeding, JVM seeding, TypeScript seeding, Elixir seeding.

For the shared workflows, see event concurrency, PII, reactor delivery identity, and event seeding.