Platform — Core concepts
These four concepts are the key to understanding all of Platform. Once you know them, the following pages become much lighter.
EntityEvent — "something just changed"
EntityEvent is a small message saying an entity just changed. Examples: "Account #123 was created", "Employee #45 was updated".
Each event carries:
- EntityType — entity type (Account, Attendee, Invoice, Employee, etc.), each with a numeric id.
- EventType / ActionType — what action happened (
Create_Account,Update_Employee, etc.), also numeric id. - Entity identity — id/guid to know exactly which entity.
- (Optional) Payload / snapshot — data snapshot for processing effects.
Service only needs to publish the event to entity-event-queue; it does not send email or write history itself. A worker reads the event and handles the effects. This is the spirit of event-driven: separate "what happened" from "what follows from it".
Side effect & Post-processor — independent effects
From one event, multiple independent side effects may be produced:
Each post-processor owns one concern (notification, history, etc.) and runs independently: one failing does not break the other. Each post-processor walks a mapping registry — a set of small mappings, each answering: "does this event produce my effect, and if yes, what does it look like?". Mappings do not save to DB themselves; the post-processor owns persistence + deduplication consistently.
Hybrid — two ways to create a notification
This is the most important architecture decision. There are two paths to create a NotificationJob:
| Path | Who creates the job | Use when |
|---|---|---|
| Direct (action-derived) | Service creates through NotificationJobFactory | Needs context computed at call time — before/after diff, request comment, dynamic recipients |
| Event-driven (event-derived) | Service only publishes event; worker creates job through mapping | Notification is a pure reaction to changed state; recipients/content can be derived from entity + settings |
Both go through the same NotificationJobFactory and the same send pipeline. The only difference is who presses the create-job button.
Why not force everything to one side?
- Do not force everything event-driven because:
- Call-time context is lost: worker only sees state after commit and cannot reconstruct before/after diff or request input.
- Over-firing ⚠️: many current events are generic CRUD (
Create_Account, etc.) fired from many places for audit. Mapping them directly to notification would send much more broadly than today → user spam.
- Do not keep everything direct because: duplicated contract in ~49 places (easy to get wrong), not idempotent (each place uses its own
Guid.NewGuid()), service is coupled to notification details.
➡️ Hybrid takes the strengths of both: event-derived gets decoupling + idempotency; action-derived keeps context. The safe way to "opt" a generic CRUD event into notification is to attach a hint (NotificationEventTypeId) at the exact guarded call-site — see Common cases.
Idempotency — deduplicate with deterministic keys
Azure Queue is at-least-once: the same event may be delivered (and processed) multiple times. If each processing creates a new row with a random key (Guid.NewGuid()), we get duplicates — email sent twice, history has two rows.
Solution: deterministic key — key derived from event identity, not random.
- Notification event-derived: uses
NotificationEventKey.ForEntityEvent(...)→ same event produces same key ⇒ retry does not duplicate. - History: uses
HistoryEventKey.ForEntityEvent(...)asRowKey+ insert-or-replace ⇒ retry overwrites exactly one row, genuinely different events get different keys (history preserved). - Action-derived (direct): intentionally keeps
Guid.NewGuid()(at-least-once), because these flows have no stable replay identity; recipients/content are computed at call-time.
Golden rule: one notification type may be produced by exactly one path (event-derived or direct, not both) — otherwise it sends twice. A dual-path guardrail test enforces this.
Next: see how these concepts assemble in Architecture.