URN Naming

A message’s identity travels in the envelope’s job field as a URN (Uniform Resource Name). The URN is the only thing a consumer uses to decide which handler runs. Because it is a plain, stable string under the application’s control — never a class name — a Go or Python consumer can route on it without sharing any type with the producer, and the producing class can be renamed or moved freely without breaking any consumer.

Format

urn:babel:<bounded-context>:<event-or-command>
SegmentMeaningRules
urnLiteral URN scheme prefix (RFC 8141).Fixed.
babelNamespace identifier for the BabelQueue ecosystem.Fixed.
<bounded-context>The domain/service area that owns the message.lowercase, [a-z0-9-].
<event-or-command>What happened (event, past tense) or what to do (command, imperative).lowercase, dot-separated sub-parts allowed.

Examples

urn:babel:orders:created
urn:babel:orders:invoice.requested
urn:babel:billing:payment.captured
urn:babel:media:thumbnail.generate
urn:babel:notifications:email.welcome.send

This convention is recommended, not enforced by the libraries (to preserve flexibility), but teams SHOULD treat it as mandatory in their own projects.

Rules

  1. Non-empty. An empty URN is a fatal producer error.
  2. Stable across deployments. A URN is a public contract; once published and consumed by another service, treat it as immutable. To change identity, introduce a new URN and migrate.
  3. Unique per message type. One URN ↔ one logical message/handler.
  4. Never a class name. App\Jobs\ProcessOrder must never appear on the wire.
  5. Lowercase & ASCII. Use [a-z0-9:._-]. No spaces, no uppercase, no unicode.
  6. No payload in the URN. Identifiers like order_id belong in data. urn:babel:orders:created ✅ — urn:babel:orders:1042 ❌.

Events vs commands

KindTense/moodExampleSemantics
Eventpast tenseurn:babel:orders:createdSomething happened; zero-to-many consumers may react.
Commandimperativeurn:babel:media:thumbnail.generateA request to do something; typically one handler.

Prefer events for decoupled, fan-out flows; use commands for directed work.

Mapping URNs to handlers

The wire identity is decoupled from code: each consuming service maps URNs to its own handler types, in whatever registry that language/framework uses. For example, in Laravel this lives in config/babelqueue.php:

'handlers' => [
    'urn:babel:orders:created'           => \App\Consumers\OnOrderCreated::class,
    'urn:babel:orders:invoice.requested' => \App\Consumers\GenerateInvoice::class,
],

The same URN routed in Go, Python, Node, Java or .NET maps to that language’s handler registry. See the consumer rules.

Versioning a message identity

When a message’s data shape changes incompatibly, do not silently reinterpret the old URN. In order of preference:

  1. Additive change (new optional field): keep the URN; no version change.
  2. Breaking change: mint a new URN with a version suffix and run both during migration, e.g. urn:babel:orders:createdurn:babel:orders:created.v2.
  3. Decommission the old URN only after all producers/consumers have migrated.

URN-level versioning is independent of the envelope’s meta.schema_version (which versions the envelope, not the payload of a specific URN).

Teams SHOULD keep a checked-in registry of every URN they own — its data shape, owning context, producers, and consumers — so the cross-service contract is discoverable. A per-URN JSON Schema for data is encouraged. This registry is project-specific and lives in your own apps, not in the BabelQueue libraries.