Lesson 12 · The repo in practice
NATS vs Kafka & ops
Why this repo runs two buses, why it's moving some flows between them, how NATS is operated — and the whole course in one view.
Your win: articulate when to reach for NATS vs Kafka, explain the live migration, know the ops essentials — and recite the whole pipeline.
NATS vs Kafka — the division of labour
| NATS JetStream | Kafka | |
|---|---|---|
| Strength | ultra-low latency, lightweight, simple ops | durability, huge throughput, long retention, replay |
| Used here for | internal service events & fan-out (user/staff/usergroup upserts, push-notification requests), the audit firehose | durable pipelines: CDC (Debezium), the outbox, email, bulk jobs, DWH sync |
| Retention | interest (keep while a consumer needs it) | time/size-based log (replayable history) |
| Naming | Staff.Upserted (dotted PascalCase) | {team}.{topic} |
The live migration: NATS → Kafka
You've seen the commented-out subscribers (Lessons 9–10). The repo is migrating some
flows from NATS to Kafka, gated by an Unleash flag
(Communication_Notification_Use_Kafka_Instead_Of_NATs). Because the domain handler is
transport-agnostic (Lesson 10), migrating a flow means swapping the transport shell,
not rewriting the logic:
Why move a flow at all? When it outgrows NATS's sweet spot — it needs durable replay, higher throughput, or to join the CDC/data pipeline. The Unleash flag lets the team flip a flow over (and back) safely. So "which bus?" is sometimes a runtime decision, and the design makes it cheap.
Operating NATS
The server
JetStream with file storage (store_dir, 4 GiB mem / 10 GiB file),
client port 4223, monitoring on 8223, a 3-replica StatefulSet
in prod (1 locally).
Per-service ACLs
nats.conf gives each service its own account that may only publish its own
subjects (and subscribe to its inboxes). fink is unrestricted (it
provisions everything). A real security boundary.
Provisioning & tests
Streams via the fink job; consumers at subscribe time. Metrics via OpenCensus
views + a disconnect gauge. Subscribers are unit-tested against a mock
JetStreamManagement (testify).
🏛️ The whole course, in one view
resource_path, re-injected on the consumer, so RLS works on async
events. 4. At-least-once + idempotent — explicit ack + dedup + idempotent handlers =
effectively-once. 5. NATS vs Kafka — fast internal events vs durable pipelines, with
a transport-agnostic handler that makes migration a swap.
NATS & Kafka compared
The trade-off in depth — latency, ops, delivery, use cases — to cement the "which bus" instinct.
→ Synadia — NATS & Kafka compared
→ docs.nats.io — Compare NATS ·
in-repo local/configs/services/nats-jetstream/nats.conf
Check yourself (from memory)
Q1. When should you reach for NATS over Kafka in this repo?
Q2. Why can a flow migrate from NATS to Kafka without a rewrite?
Q3. What do the per-service NATS account ACLs enforce?
fink is unrestricted.
{team}.{topic}). Rule: NATS =
"tell interested services, then forget"; Kafka = "capture durably + stream". Migration:
active NATS→Kafka (Unleash flag …Use_Kafka_Instead_Of_NATs); the transport-agnostic
handler makes it a shell swap, not a rewrite. Ops: JetStream file storage (4GiB/10GiB),
client :4223 / monitoring :8223, 3-replica StatefulSet (1 local); per-service ACLs
(publish own subjects only; fink unrestricted); streams via fink; mock JetStreamManagement
for tests.handleMsg heart,
delivery) → the repo in practice (your conversationmgmt & notification
subscribers, the publish link, the firehose, NATS vs Kafka). You can now open any subscriber you own
and read it fully — and name what the repo really does: tenancy through the envelope so RLS
survives the hop, Ack-only redelivery with no DLQ, the transport-agnostic handlers, and the live
migration. Interview-grade fluency in the eventing your services live on.
1. Synadia — NATS & Kafka compared. In-repo: local/configs/services/nats-jetstream/nats.conf, internal/notification/CLAUDE.md (the migration flag).