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.
Appending and concurrency
Section titled “Appending and concurrency”Default concurrency checks
Section titled “Default concurrency checks”| Client | Append without an explicit concurrency scope |
|---|---|
| .NET | The 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 Java | Use ConcurrencyScope.none; no optimistic concurrency check is requested. |
| TypeScript | Sends an unset expected sequence number; no optimistic concurrency check is requested. |
| Elixir | Sends 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.
Expecting no matching event
Section titled “Expecting no matching event”.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.
Results and rejections
Section titled “Results and rejections”| Client | Result shape |
|---|---|
| .NET | AppendResult exposes the appended position and rejection details. AppendManyResult carries the batch’s sequence numbers and violations. |
| Kotlin and Java | AppendResult per event; a batch returns a list. Java can read the position through getSequenceNumberValue(). |
| TypeScript | AppendResult 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.
Compliance and PII
Section titled “Compliance and PII”Resolving the subject on append
Section titled “Resolving the subject on append”| Client | When no explicit append subject is supplied |
|---|---|
| .NET | Resolves an event’s [Subject] property before appending. The kernel falls back to the event source id when no subject is set. |
| Kotlin and Java | Default to the event source id. The append path does not resolve event-side @Subject metadata. |
| TypeScript | Uses options.subject ?? eventSourceId; the append path does not resolve an event’s @subject property. |
| Elixir | Sends 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.
Authorizing a new key after erasure
Section titled “Authorizing a new key after erasure”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.
Reactor replay and delivery identity
Section titled “Reactor replay and delivery identity”Replay eligibility
Section titled “Replay eligibility”| Client | Registration and handler behavior |
|---|---|
| .NET | Reactors are replayable unless the class has [OnceOnly]. Method-level [OnceOnly] skips that handler during replay; [Replay] selects a replay handler. |
| Kotlin and Java | Kotlin registration and dispatch use @OnceOnly and @Replay; Java reactors go through the same JVM registration. |
| TypeScript | Method-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. |
| Elixir | Reactor 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.
Delivery identity
Section titled “Delivery identity”.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.
Event seeding
Section titled “Event seeding”| Client | Default target and duplicate handling | Seeder callback failure |
|---|---|---|
| .NET | For/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. |
| TypeScript | for/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 Java | Entries target the selected namespace, defaulting to the client’s namespace. Uses kernel entry tracking, but does not send seed tags. | A callback exception propagates. |
| Elixir | Global 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.