Lesson 2 · Core NATS & the JetStream model
Subjects & pub/sub
How messages are addressed — a dotted hierarchy with wildcards — and the queue groups that let many subscribers share the load.
Your win: read the repo's subject names, use the two wildcards, and explain how a queue group turns "everyone gets it" into "exactly one of us gets it."
Subjects: the address
A subject is where a message is published — NATS's equivalent of a topic, but hierarchical and fine-grained. This repo names them dotted PascalCase, one per event:1
SubjectUserCreated = "User.Created"
SubjectUpsertStaff = "Staff.Upserted"
SubjectUpsertUserGroup = "UserGroup.Upserted"
SubjectNotificationCreated = "Notification.Created"
SubjectActivityLogCreated = "ActivityLog.Created"
Every event name has a small family of constants beside it — Subject… (the
address), Stream… (the store, Lesson 3), Queue…/Durable…/Deliver…
(the consumer, Lesson 4). Learn to read the family and you can read any subscriber.
Publish/subscribe: one-to-many
The default NATS model: a publisher sends on a subject, and every subscriber
interested in that subject receives a copy.1 The publisher
doesn't know or care who's listening — total decoupling. In the repo, usermgmt publishes
Staff.Upserted, and both conversationmgmt subscribers (agora + conversation)
independently receive it, plus anyone else who cares.
Wildcards: subscribe to a family
Subjects are hierarchical, so subscriptions can match patterns. Two wildcards:2
| Wildcard | Matches | Example |
|---|---|---|
* | exactly one token | User.* → User.Created, User.Deleted (not User.Foo.Bar) |
> | one or more trailing tokens (the rest) | chat.chat.> → everything beneath it |
user stream grabs
User.*, so all user events land in one store. A consumer then filters
down to a concrete subject — conversationmgmt's consumer sets FilterSubject = "User.Created"
to receive only that one (Lesson 4). Capture broadly, consume narrowly.
Queue groups: load-balance the subscribers
Plain pub/sub sends every message to every subscriber — but if your service runs three replicas, you usually want each message handled once, not three times. A queue group does exactly that: subscribers sharing a queue-group name form a pool, and the server delivers each message to exactly one member (chosen for you).3
This is how the repo scales a subscriber horizontally: every conversationmgmt pod joins the same
queue group (Queue… constant), so three pods share the work rather than triplicating it.
It's the reason nearly every subscription here is a QueueSubscribe, not a plain
Subscribe.
Subjects, pub/sub & queue groups
The addressing model and the two distribution patterns — the vocabulary the rest of the course reuses.
→ docs.nats.io — Subjects & wildcards
→ Publish/Subscribe ·
Queue Groups
Check yourself (from memory)
Q1. What does the * wildcard match in a subject?
* = one token; > = the rest. Streams
capture with */>; consumers filter to a concrete subject.
Q2. What does a queue group do?
Q3. How do streams and consumers use wildcards differently?
user stream
grabs User.*; a consumer sets FilterSubject = "User.Created".
User.Created, Staff.Upserted, Notification.Created), each with
a family (Subject/Stream/Queue/Durable/Deliver). Pub/sub = one-to-many:
every interested subscriber gets a copy; publisher fully decoupled. Wildcards:
* = one token (User.*); > = the rest (chat.chat.>).
Streams capture broadly (User.*); consumers FilterSubject narrowly
(User.Created). Queue group = each message → exactly one
member (load balancing) → how N replicas share work; hence QueueSubscribe everywhere.
All core NATS (at-most-once here); JetStream keeps the vocabulary + adds the store.* for one token,
> for the rest. Plain pub/sub is one-to-many, but a queue group load-balances:
each message goes to exactly one member, so replicas of a service share the work. Our streams
capture a wildcard and consumers filter to a concrete subject."
1. docs.nats.io — Publish/Subscribe. In-repo: internal/golibs/constants/common.go.