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>
| Segment | Meaning | Rules |
|---|---|---|
urn | Literal URN scheme prefix (RFC 8141). | Fixed. |
babel | Namespace 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
- Non-empty. An empty URN is a fatal producer error.
- 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.
- Unique per message type. One URN ↔ one logical message/handler.
- Never a class name.
App\Jobs\ProcessOrdermust never appear on the wire. - Lowercase & ASCII. Use
[a-z0-9:._-]. No spaces, no uppercase, no unicode. - No payload in the URN. Identifiers like
order_idbelong indata.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:
'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:
- Additive change (new optional field): keep the URN; no version change.
- 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. - 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.