Skip to content

Migrate append routing

The next major client release stops choosing routing defaults on your behalf. Before upgrading, decide whether new events should use the kernel’s defaults or continue the routes your application already uses. Existing stored events are not moved or rewritten.

With the matching kernel release, omitted or empty routing resolves as follows. Upgrade every kernel node before deploying the new client. The client verifies its contract descriptor before exposing each channel; an unsupported kernel cannot receive an append.

Route fieldPrevious single/rich-batch defaultKernel-owned default
eventSourceTypeDefaultDefault
eventStreamTypeDefaultAll
eventStreamIdThe event-source idDefault

This is a breaking routing change, not just a serialization change. Review stream-filtered observers, queries, and concurrency scopes before switching. Earlier single-source appendMany calls also dropped supplied routing options; they now honor them for every event in the batch.

A missing value (null) and an empty string both leave the decision to the kernel. The client never replaces an explicit nonempty route, including Default, All, or a stream id equal to the event-source id. An empty string is not a way to select a legacy route.

For single appends, set all three route fields to the values you intend to keep. These helpers accept your application’s annotated event object.

import io.cratis.chronicle.eventSequences.AppendOptions
import io.cratis.chronicle.eventSequences.IEventSequence
suspend fun appendToLegacyRoute(
sequence: IEventSequence,
eventSourceId: String,
event: Any
) = sequence.append(
eventSourceId,
event,
AppendOptions(
eventSourceType = "Default",
eventStreamType = "Default",
eventStreamId = eventSourceId
)
)
import io.cratis.chronicle.eventSequences.AppendResult;
import io.cratis.chronicle.java.AppendOptionsBuilder;
import io.cratis.chronicle.java.BlockingEventSequence;
class LegacyRouting {
static AppendResult appendToLegacyRoute(
BlockingEventSequence sequence,
String eventSourceId,
Object event) {
var options = new AppendOptionsBuilder()
.eventSourceType("Default")
.eventStreamType("Default")
.eventStreamId(eventSourceId)
.build();
return sequence.append(eventSourceId, event, options);
}
}

Pass the same options to appendMany(eventSourceId, events, options) for a single-source batch. For a batch of EventForEventSourceId values, set eventSourceType, eventStreamType, and eventStreamId on each value; use that value’s own event-source id for the legacy stream id. Mixed explicit and omitted routes remain mixed rather than inheriting a route from another event.

If your application previously passed options to single-source batches, check stored event contexts instead of assuming those options took effect. To continue a stored route, supply its actual values explicitly.

Keep concurrency and compliance choices separate

Section titled “Keep concurrency and compliance choices separate”

No concurrency defaults change: ordinary appends still use ConcurrencyScope.none when omitted; an explicitly supplied none or notSet is preserved. Rich batches still send exactly the scope-map entries you supplied. A source omitted from that map stays omitted. Routing fields are never copied into concurrency scopes automatically.

Subject behavior also stays unchanged: an omitted subject falls back to the event-source id, while an explicit subject is forwarded unchanged. Occurred time, tags, causation, identity, and correlation are retained when a single-source batch uses explicit routing.

Reads narrow only by what you pass. A missing or empty filter means “do not narrow”, which is how the kernel reads an unspecified value - the client never substitutes a route of its own on a read, a tail lookup, or an observer tail.

The consequence is worth stating plainly, because it is the easiest way to lose sight of new events after upgrading: an append that named no route is stored on stream type All and stream id Default. A read narrowed to the route this client used to choose - eventStreamType = "Default" - therefore returns none of those events. It is a filter doing its job, not missing data.

import io.cratis.chronicle.eventSequences.AppendedEvent
import io.cratis.chronicle.eventSequences.IEventSequence
import kotlin.reflect.KClass
/** Every event for the source, whatever route the kernel resolved. */
suspend fun readEveryRoute(
sequence: IEventSequence,
eventSourceId: String,
eventTypes: List<KClass<*>>
): List<AppendedEvent> =
sequence.getForEventSourceIdAndEventTypes(eventSourceId, eventTypes)
/** Only the events the kernel routed by default. */
suspend fun readKernelDefaultRoute(
sequence: IEventSequence,
eventSourceId: String,
eventTypes: List<KClass<*>>
): List<AppendedEvent> = sequence.getForEventSourceIdAndEventTypes(
eventSourceId,
eventTypes,
eventStreamType = "All",
eventStreamId = "Default"
)

getForEventSourceIdAndEventTypes also takes an eventSourceType, which narrows the read to one event source type. Omitting it, or passing Default, returns every source type. It reached nothing before Chronicle 18.5.0 — the request carried no such field, so a caller that passed it believed it had filtered and had not.

Read newly appended events back and check their route and metadata using getForEventSourceIdAndEventTypes or getFromSequenceNumber. These reads carry stored routing rather than guessing it from the append request.

EventContext now carries the compliance subject the kernel stored, which changes its constructor and copy signatures. Recompile JVM consumers, including Java callers, when upgrading.