Communication — Core concepts
Artifacts and "grain"
"Grain" = the level of detail represented by one record (what one record stands for).
| Artifact | Store | Grain | Role |
|---|---|---|---|
| NotificationJob | Azure Table | 1 notification event | Input: "there is work to send". |
| NotificationScheduler | SQL | 1 pending job | Queue row for Scheduler to pick up. |
| MessagingJob + Blob | SQL + Blob | 1 large payload | Coordinates campaign/report (large content lives in Blob). |
| UserMessage | Azure Table | 1 recipient | In-app message; inbox in the app. |
| OutgoingMessage | Azure Table | 1 recipient × 1 channel | Message sent through provider; carries delivery state. |
Send pipeline: three steps
| Step | What it does |
|---|---|
| Scheduler | Scans Ready work; evaluates Notification Settings (is this type enabled, who receives it). |
| Generator | Renders templates + resolves recipients; creates UserMessage (in-app) and/or OutgoingMessage (external channel). |
| Distributor | Sends for real through SendGrid/Twilio; "claim before send". |
| Webhook & Tracker | Updates state (delivered/bounce/opened) back to OutgoingMessage. |
Four core principles
Every Communication decision follows from 4 principles (shared with automation):
Identity keys instead of random keys — each occurrence has exactly one deterministic key; every downstream key (OutgoingMessage, etc.) is derived from it. Duplicate events/retries produce the same key → no duplicates.
Ensure*instead ofCreate*— every artifact creation operation means "ensure it exists": already exists and compatible → always success (idempotent); already exists but content differs (InputHashdiffers) → conflict, stop automatic sending, report support. NeverUpsert Replace, to avoid accidentally sending again.Claim before send — Distributor must move
OutgoingMessagetoSendingwith a conditional update before calling the provider. Duplicate queue messages that see the row alreadySending/Sentexit — users do not receive duplicate emails.Bounded reconciliation instead of outbox — no complex outbox; each provider declares a bounded scan-repair path (watermark, date window, batch) that runs at low frequency to repair rare issues.
⚠️ Exception to the no-replace principle:
EntityHistory(audit) may upsert InsertOrReplace by deterministic key, because itsRowKeyis not externally referenced and overwriting on retry is harmless.NotificationJobkeeps query-before-append.
Template
Message content is a template with {{...}} placeholders (for example {{INCIDENT_DETAILS_LINK}}). Generator fills real data at render time. A template has key + version (part of the "immutable routing fields" — not changed mid-send).
Immutable vs mutable fields
When Ensure* matches an existing artifact, it distinguishes:
- Immutable (message identity): source coordinates, event type, recipient identity, delivery method, template key/version, run key, automation coordinates. Mismatch → conflict, stop send.
- Mutable (safe to change): retry counters, scheduler coordinates, provider status, webhook timestamps, bounded diagnostics.
Got it? See how they assemble into the system in Architecture.