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

internal/golibs/constants/common.go
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

WildcardMatchesExample
*exactly one tokenUser.*User.Created, User.Deleted (not User.Foo.Bar)
>one or more trailing tokens (the rest)chat.chat.> → everything beneath it
How wildcards are used here A JetStream stream captures with a wildcard — the 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

publish User.Created ──▶ [ replica-1 ] ┐ [ replica-2 ] ← │ queue group "queue-conversationmgmt-user-created" [ replica-3 ] ┘ → the server picks ONE to deliver to

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.

Core NATS caveat, one more time Everything in this lesson is core NATS — subjects, pub/sub, queue groups exist without JetStream, and at this level delivery is at-most-once. JetStream (Lesson 3) keeps the same subject/queue-group vocabulary but adds the durable store behind it, upgrading delivery to at-least-once. Same addressing, persistent backing.
Read this next

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?

Load balancing: one message → one member. It's how three replicas of a service share the work instead of triplicating it.

Q3. How do streams and consumers use wildcards differently?

Capture broadly, consume narrowly: the user stream grabs User.*; a consumer sets FilterSubject = "User.Created".
Recall: subjects, pub/sub, wildcards, queue groups.
address + one-to-many + wildcards + load-balance, then reveal
Subject = the message address; dotted PascalCase here (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.
🎯 Interview one-liner "How does NATS address and distribute messages?" → "Messages are published to hierarchical dotted subjects; subscribers match exact subjects or wildcards — * 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."
Next: turning fire-and-forget into durable — JetStream and streams: how messages get stored, the retention policies, and who provisions them. Ask me about wildcards or queue groups if they're fuzzy.

1. docs.nats.io — Publish/Subscribe. In-repo: internal/golibs/constants/common.go.

2. docs.nats.io — Subjects & wildcards.

3. docs.nats.io — Queue Groups.