Skip to content

Identify a command with a key

Most commands act on something that already exists — a specific order, a specific account. Arc needs to know which one, before your handler runs, so that it can resolve the current state the command operates on and so that integrations can route what the command produces.

You tell it by marking the identifying property with @CommandKey.

@Command
public record ChangeShippingAddress(@CommandKey OrderId orderId, Address address) {
public void handle(@FromServices OrderRepository orders) {
orders.updateAddress(orderId, address);
}
}

View Java snippet source on GitHub

A command without a key is perfectly valid — a command that creates something has nothing to identify yet.

@CommandKey may appear on one property. Two of them is ARCKSP0106, a compile error, because Arc would otherwise have to guess which identity the command means and would guess consistently wrongly for one of them.

The annotation is accepted on a property, a field, a value parameter or a property getter, which covers Kotlin data classes, Kotlin classes with explicit properties, and Java records alike.

When the key is not a single declared property — it is derived from two of them, or comes from somewhere else on the command — implement CommandKeyProvider:

@Command
public record ArchiveTenantData(String tenant, int year) implements CommandKeyProvider {
@Override
public Object commandKey() {
return tenant + "/" + year;
}
public void handle(@FromServices ArchiveService archive) {
archive.run(tenant, year);
}
}

View Java snippet source on GitHub

CommandKeyProvider is a fun interface with a single commandKey() method returning Any?, so a Java record or class implements it without ceremony. From Kotlin you can also read it as a property: command.key.

Return null to say this command has no key, which is the same as not declaring one.

The key identifies the command instance. It is not a route parameter and not a validation concern — the value is not checked for existence, and declaring a key does not by itself load anything.

Where it matters is in integrations that need to know what the command is about. The Chronicle integration uses it to decide the event source a command’s events belong to; a command that returns events without a resolvable key is rejected at that point rather than appending to the wrong stream.

The C# implementation spells the same two mechanisms [Key] and ICanProvideKeyForCommand.GetKey(). The concept, the “exactly one” rule and the role in resolving command state are the same. The types differ: C#‘s GetKey() returns a string, while the JVM CommandKeyProvider.commandKey() returns Any?, so each integration decides which key types it accepts. The Chronicle integration accepts String, UUID, a number, or a concept wrapping one of those. The parity reference tracks the exact status.