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
data class ChangeShippingAddress(
@CommandKey val orderId: OrderId,
val address: Address
) {
suspend fun handle(@FromServices orders: OrderRepository) {
orders.updateAddress(orderId, address)
}
}

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
class ArchiveTenantData(val tenant: String, val year: Int) : CommandKeyProvider {
override fun commandKey(): Any = "$tenant/$year"
suspend fun handle(@FromServices archive: ArchiveService) {
archive.run(tenant, year)
}
}

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; only the names differ.