Files
clean-architecture-backend-…/docs/notification/operations.md
T
DongHyeonkaandClaude Opus 5 701ba67456 feat(notification): implement the notification delivery platform
Maps the 31-module plan onto the registry's 19 leaves as packages; the two
edges the registry forbids (provider->httpclient, inbox->messaging) are
replaced by application-owned ports. See docs/notification/module-mapping.md.

Acceptance is not delivery: ProviderSubmissionResult refuses to carry a
delivery outcome, and AMBIGUOUS is a first-class terminal state that blocks
automatic retry and fallback until reconciliation resolves it.

Providers: SES (SigV4 + SNS callback), Twilio (X-Twilio-Signature +
reconciliation), FCM (FID-primary batch), APNs, Web Push (RFC 8030/8291/8292),
SMTP and webhook. Contact points are AES-256-GCM encrypted with a separate
HMAC lookup fingerprint; nothing raw reaches a log, metric tag or exception.

Dispatch commits the attempt row, calls the provider with no transaction open,
then records the outcome; the durable queue uses FOR UPDATE SKIP LOCKED.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:57:27 +09:00

2.1 KiB

Operations

Runtime shape

durable queue (PostgreSQL, FOR UPDATE SKIP LOCKED)
  → expiry check
  → suppression and eligibility re-check
  → provider health gate
  → rate limiter
  → concurrency limiter
  → provider adapter

Provider calls run outside every database transaction. The attempt row is committed first, so after a crash the row is either absent (nothing was sent) or present in DISPATCHING (reconciliation has something to ask about).

Guards that exist for specific incidents

Guard The incident it prevents
Credential failure opens the provider route One expired key multiplied by a queue becomes a self-inflicted outage
Retry budget per provider profile A provider outage turning every queued notification into its own retry loop
Ambiguous attempts block automatic fallback A push whose response was lost arriving alongside the "just in case" SMS
Permits released during backoff A slow provider pinning the whole concurrency budget on work that is only waiting
Bounded drain on rotation A provider that never answers holding a credential rotation open forever
Fail-fast intake on capacity An unbounded in-memory queue absorbing a burst it cannot survive

Scheduling

scheduleAt activates the job, notBefore is the earliest permitted provider submission, and expiresAt blocks new attempts, retries and fallbacks. Suppression and expiry are re-checked immediately before dispatch, because a scheduled notification can sit in the queue for hours and the user may have opted out in the meantime.

Redrive

A redrive preserves NotificationId and RecipientDeliveryId, creates a new DeliveryAttemptId, and reuses the pinned template version and rendered digest. Sending different content is a new notification, not a redrive. Redriving an ambiguous attempt requires explicit duplicate-risk approval, because the platform genuinely cannot tell whether the first submission reached the user.

Actuator surface

Provider runtime states and generations, queue depth and age, callback and reconciliation health. Never addresses, never credentials.