Skip to content

Handlers and wire names

Everything the platform stores or routes by has a name you declare as a string, separate from the name of the class or method in your code. A component has a component id, each handler has a wire name, and each stored type has a manifest. Those strings are protocol: they are written into the database and used to address calls. The code names are yours to change at any time.

Declaring handlers

A handler is declared under its wire name, beside the method that implements it.

Scala

object ShoppingCartEntity
    extends EventSourcedEntity.Companion[ShoppingCartEntity, ShoppingCart, ShoppingCartEvent](
      componentId = ComponentId("shopping-cart"),
      stateSerializer = Codecs.serializer[ShoppingCart]("shopping-cart"),
      eventSerializer = Codecs.serializer[ShoppingCartEvent]("shopping-cart-event")
    ):
  def create(context: EventSourcedEntityContext) = new ShoppingCartEntity(context)

  val addItem = command("add-item")(_.addItem)
  val getCart = query("get-cart")(_.getCart)

Python

class ShoppingCartEntity(EventSourcedEntity[ShoppingCart, ShoppingCartEvent]):
    component_id = "shopping-cart"
    state_codec = json_codec(ShoppingCart, "shopping-cart")
    event_codec = json_codec(ShoppingCartEvent, "shopping-cart-event")

    @command("add-item")
    def add_item(self, item: LineItem) -> EventSourcedEffect[ShoppingCart, ShoppingCartEvent, Done]: ...

    @query("get-cart")
    def get_cart(self) -> ReadOnlyEffect[ShoppingCart, ShoppingCartEvent, ShoppingCart]: ...

In Scala the declaration is a value on the companion object, and a call site uses that value:

componentClient.forEventSourcedEntity(cartId).call(ShoppingCartEntity.addItem).invoke(item)

The call is fully typed — the argument and the reply type come from the declaration — and involves no reflection. There is no classpath scanning and no inspection of method names or bytecode. In Python a call names the component id and the wire name as strings:

await client.for_event_sourced_entity("shopping-cart", cart_id).call("add-item").invoke(item, reply=Done)

Because Scala and Python address a handler by the same component id and wire name, and store data under the same manifests, a service can be ported from one language to the other without its callers or its stored data noticing.

Why the wire name is not the method name

A method name is a detail of the code. A wire name is a promise to everything outside it:

  • During a rolling update, old and new instances run side by side and call each other. A call sent by an old instance names the handler as the old code knew it. If the name came from the method, renaming a method would make every such call fail until the update finished.
  • Timers are stored. A timer holds the component id and the handler's wire name of the call it will make, possibly hours or days later. A handler renamed in the meantime would leave the timer calling a name that no longer exists, and it would be retried forever.
  • Other services and other languages address handlers by name, and cannot see your code at all.

Declaring the name separately makes the boundary visible. Renaming a method is a refactoring; changing a string in a command(...) or @command(...) is a protocol change, and it looks like one in review.

What each name is used for

Name Declared as Used for
Component id ComponentId("shopping-cart"), component_id = "shopping-cart" addressing calls; the journal's entity type; view table names; timers; traces
Wire name command("add-item"), @command("add-item"), and likewise query, workflow step, timed action handler addressing one handler; timers; traces
Manifest Codecs.serializer[T]("shopping-cart-event"), json_codec(T, "shopping-cart-event") stored with every event, snapshot, state and row, to say how to read it
Route path get("/{cartId}"), @get("/{cartId}") the service's public HTTP API

What is safe to change

Change Safe? Why
Rename a Scala method, Python method, class or package yes nothing outside the code knows these names
Add a handler, a component or a route yes nothing addresses it yet
Remove a handler nothing calls any more yes, once no caller or timer uses it a stored timer that names it will fail on every attempt
Change a handler's wire name no in-flight calls, stored timers and other services use the old name
Change a component id no the journal, snapshots, view tables and timers are keyed by it; the component would start empty
Change a manifest no stored data carries the old manifest and could no longer be read
Change a route path a breaking API change callers outside the service use it

To change a name that is not safe to change, add the new one beside the old, move callers across, and remove the old one only when nothing can still use it: no instance running old code, no timer still scheduled, no caller outside the service. For stored types, see Serialization and evolution.

Names are lowercase and hyphenated by convention

ankka does not require a style for wire names and component ids, but every sample uses lowercase words joined by hyphens: shopping-cart, add-item, get-cart. Traces and the local console show a call as <component id>#<wire name>, for example shopping-cart#add-item, so consistent names read well there. A service's own name, in its descriptor, does have rules, because it becomes a Kubernetes name and part of a hostname: lowercase letters, digits and hyphens, starting with a letter, at most 63 characters.