--- title: "URN Naming" description: "How every BabelQueue message is identified — a stable urn:babel:: string instead of a class name, so any language can route on it." source: https://babelqueue.com/docs/spec/1.x/urn/ updated: 2026-06-07T00:00:00.000Z --- # URN Naming A message's identity travels in the envelope's [`job`](/docs/spec/1.x/envelope/) 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:: ``` | Segment | Meaning | Rules | | :--- | :--- | :--- | | `urn` | Literal URN scheme prefix (RFC 8141). | Fixed. | | `babel` | Namespace identifier for the BabelQueue ecosystem. | Fixed. | | `` | The domain/service area that owns the message. | lowercase, `[a-z0-9-]`. | | `` | 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 | Kind | Tense/mood | Example | Semantics | | :--- | :--- | :--- | :--- | | **Event** | past tense | `urn:babel:orders:created` | Something *happened*; zero-to-many consumers may react. | | **Command** | imperative | `urn:babel:media:thumbnail.generate` | A 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`: ```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](/docs/spec/1.x/envelope/#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:created` → `urn: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*). ## Registry (recommended practice) 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.