Lesson 10 · The repo in practice · your services

notification & the publish link

Your two services, connected by one subject — conversationmgmt publishes, notification consumes, and the domain logic doesn't care which bus carried it.

Your win: read notification's subscriber, trace the Notification.Created link from conversationmgmt to notification, and explain why the business logic is transport-agnostic.

The subscriber: Notification.Created

notification's core NATS subscriber is the mirror image of Lesson 9 — same shape, its own tuning:

internal/notification/transports/nats/push_notification.go:30-47
func (i *PushNotificationEvent) StartSubscribe() error {
    opts := nats.Option{ JetStreamOptions: []nats.JSSubOption{
        nats.ManualAck(),
        nats.Bind(StreamNotification, DurableNotification),
        nats.DeliverSubject(DeliverNotification),
        nats.MaxDeliver(3), nats.AckWait(90*time.Second), nats.MaxAckPending(30),
    }}
    return i.nats.QueueSubscribe(SubjectNotificationCreated, QueueNotification, opts, i.HandleMessage)
}

Unlike conversationmgmt, notification owns the notification stream (it's declared in fink/streams/notificationmgmt.go, Lesson 3) — so it's both the stream owner and a consumer of it. Its AckWait is 90s (sending a push can be slow).

The link: convo publishes → notification consumes

Here's the whole reason both are your services — they're connected by one subject. When a chat message needs a push, conversationmgmt publishes (Lesson 6's real example):

conversationmgmt notification PushNotificationForConversation PushNotificationEvent.HandleMessage TracedPublish(ctx, …, Notification.Created) ──────▶ QueueSubscribe(Notification.Created) (DataInMessage envelope: payload + tenant) → unmarshal → ProcessPushNotification → FCM push

That's a complete cross-service event flow you own end to end: conversationmgmt decides "a push is needed," puts a NatsCreateNotificationRequest on Notification.Created, and notification — which has no idea conversationmgmt exists — picks it up and sends the Firebase push. Total decoupling through one subject.

The handler is transport-agnostic

push_notification.go:50-82 (HandleMessage, condensed)
proto.Unmarshal(data, &notification)                    // protocol: bytes → the request
switch method {
case "push_notification":
    i.notificationSubscriber.ProcessPushNotification(ctx, &notification)   // ← the DOMAIN logic
}
return false, err                                       // retry=false → drop on error (no redelivery)
The NATS transport is a thin shell Look at what HandleMessage actually does: unmarshal, dispatch, done. The real work — ProcessPushNotification — lives in internal/notification/subscribers/, a transport-agnostic domain handler. And here's the payoff: the same handler is reused by the Kafka transport too (per the service's own CLAUDE.md). So the NATS/Kafka choice is just which shell wraps the same business logic — which is exactly what makes the NATS→Kafka migration (Lesson 12) a transport swap, not a rewrite.
A subtle delivery choice — notification drops on error Notice HandleMessage returns retry=false even on error. By Lesson 8's logic, that means a failed push is acked and dropped, not redelivered — a deliberate call: a malformed or un-sendable notification shouldn't loop forever hammering FCM. (conversationmgmt's subscribers, by contrast, return retry=true so a transient DB failure retries.) The retry bool is a per-handler policy decision, and these two of your services chose differently — for good reasons.
Read this next

notification's transport & domain split

The service's own guide explains the transport-agnostic subscribers/ reused by NATS and Kafka — the design behind the migration.

→ in-repo internal/notification/CLAUDE.md ("Event flows") · internal/notification/transports/nats/push_notification.go
→ the convo publisher …/conversation/infrastructure/nats/notification.go

Check yourself (from memory)

Q1. How are conversationmgmt and notification connected?

One subject: convo TracedPublishes to Notification.Created; notification's QueueSubscribe picks it up. Total decoupling.

Q2. Why is ProcessPushNotification not inside the NATS transport?

The transport is a thin shell (unmarshal + dispatch); the domain logic lives in subscribers/, reused by NATS and Kafka — which makes migration a swap.

Q3. notification's handler returns retry=false on error. What does that do?

retry=falsehandleMsg acks (drops). A deliberate choice so a bad push doesn't loop. (convo's subs use true to retry.)
Recall: notification's subscriber + the publish link + transport-agnostic.
subscriber + the link + the split, then reveal
Subscriber: PushNotificationEvent.StartSubscribe (push_notification.go:30-47) = QueueSubscribe(Notification.Created, QueueNotification), Bind(StreamNotification, DurableNotification), DeliverSubject, MaxDeliver 3, AckWait 90s. notification owns the notification stream (via fink). The link: conversationmgmt TracedPublishes Notification.Created → notification consumes → ProcessPushNotification → FCM push. Total decoupling via one subject. Transport-agnostic: HandleMessage = unmarshal + dispatch only; the domain logic (ProcessPushNotification in subscribers/) is reused by NATS AND Kafka → migration = swap the shell. Delivery choice: notification returns retry=false (drop failed pushes, don't loop); convo returns true (retry transient failures).
🎯 Interview one-liner "Walk me through a cross-service event you own." → "conversationmgmt decides a push is needed and publishes a Notification.Created event; notification consumes it and sends the Firebase push — the two are fully decoupled through one subject. And the business logic sits in a transport-agnostic handler reused by both NATS and Kafka, so the transport is just a thin shell — the reason we can migrate a flow from NATS to Kafka without touching the domain code."
Next: the one flow that behaves differently — the activity-log firehose and the only pull consumer in the codebase. Ask me about the retry policy choice if the drop-vs-redeliver contrast is interesting.

1. In-repo: internal/notification/transports/nats/push_notification.go:30-82, internal/notification/CLAUDE.md, …/conversation/infrastructure/nats/notification.go.