Files
clean-architecture-backend-…/docs/notification/adr/NOTIF-ADR-003-ambiguous-submission.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

1.5 KiB

NOTIF-ADR-003 — ambiguous submission is a first-class state

Status

Accepted.

Context

The most common serious failure is not a rejection. It is a request whose body reached the provider and whose response never came back. The platform has no provider request id, and the user may or may not have received the notification.

Treating that as a failure produces duplicates: a retry sends a second message, and a cross-channel fallback sends the SMS next to the push that already arrived. Treating it as a success loses real failures.

Decision

AMBIGUOUS is a stored SubmissionOutcome and AttemptConfirmation. Attempts record requestStarted, requestBodyCommitted and providerResponseReceived, each with an EvidenceCertainty of PROVEN, INFERRED or UNKNOWN, so an adapter that does not know is not forced to answer false.

While an ambiguous attempt exists on a recipient delivery:

  • automatic retry is blocked unless the provider proves per-request idempotency
  • automatic cross-channel fallback is blocked unconditionally
  • reconciliation runs where the provider supports a status query
  • otherwise the delivery stops and waits for an operator

Operator redrive of an ambiguous attempt requires explicit duplicate-risk approval.

Consequences

Some notifications stop in a state that needs a human or a reconciliation pass. That is the intended trade: an unresolved unknown is cheaper than a guaranteed duplicate, and the state is visible rather than silently resolved in either direction.