Short answer: keep an order-receipt template in the application repository, render it from a versioned payment event, and put delivery behind a narrow adapter. Apply the same ownership boundary to SMS OTP messages, but treat OTP state differently: store a keyed digest, expire it, cap attempts, rate-limit sends, and separate user cooldowns from transport retries. Airline disruption alerts need another queue so a schedule-change burst cannot delay login.
The evaluation constraint comes first. A settled payment should create one auditable receipt, replaying the event should not create another logical message, and a delayed send must not make a stale OTP useful. I would test those properties before comparing transports. If an AI model drafts variants, schema checks, snapshots, required text, and token usage belong in the release evaluation; production sends should use the reviewed artifact.
Why should the application own the template?
The simple approach puts copy in a delivery dashboard. It is quick until the event schema changes, the dashboard and worker drift, and a code rollback does not restore the matching copy. Repository ownership lets one review cover wording, escaping, required fields, and migration logic.
There is a real trade-off. Non-engineers cannot publish independently unless the deployment process provides a controlled path. If receipt wording is part of the transaction contract, reproducibility wins. If publication speed dominates, remote templates still need version pinning, approval history, and exportable artifacts.
Email authentication is separate. DMARC defines domain policy and reporting around identifier alignment; it does not prove that a receipt body is correct. Template tests do not replace SPF, DKIM, or DMARC.
The application should pass rendered content and a business idempotency key through a generic port. Provider types stay outside the domain.
from dataclasses import dataclass
from decimal import Decimal
from typing import Protocol
@dataclass(frozen=True)
class OrderPaid:
event_id: str
order_id: str
email: str
total: Decimal
currency: str
class Sender(Protocol):
def send_email(self, *, recipient: str, subject: str,
text: str, idempotency_key: str) -> None: ...
def send_receipt(event: OrderPaid, sender: Sender) -> None:
sender.send_email(
recipient=event.email,
subject=f"Receipt for order {event.order_id}",
text=f"Payment settled. Total: {event.currency} {event.total:.2f}",
idempotency_key=f"receipt:{event.event_id}",
)
A key string guarantees nothing by itself. Durable state must enforce it. Record a delivery intent while consuming the settled-payment event, then let a worker claim that intent. A retry resumes the same intent instead of creating a second receipt.
That boundary is deliberate.
This is the notebook-to-production jump. Three attractive examples are not coverage. Test escaping, fractional and zero-decimal currency formatting, text and HTML snapshots, and old event versions still in the queue. Freeze accepted AI-drafted copy rather than calling a model on the payment path; that removes nondeterministic wording and makes prompt cost measurable during drafting.
How should an SMS OTP login backend send and verify codes?
Generate codes with a cryptographically secure generator. Persist a keyed digest plus challenge ID, destination binding, expiry, attempts remaining, and consumed state. Never log the code.
NIST SP 800-63B states that out-of-band secrets are accepted only once, must be invalid if not completed within 10 minutes, and require rate limiting when their output has less than 64 bits of entropy. Ten minutes is a ceiling, not a recommended lifetime. Pick a shorter expiry only after measuring delivery latency and accessibility needs.
import hashlib
import hmac
import secrets
import time
def issue(secret_key: bytes, ttl: int = 300) -> tuple[str, bytes, float]:
code = f"{secrets.randbelow(1_000_000):06d}"
digest = hmac.new(secret_key, code.encode(), hashlib.sha256).digest()
return code, digest, time.time() + ttl
def matches(secret_key: bytes, digest: bytes, candidate: str) -> bool:
value = hmac.new(secret_key, candidate.encode(), hashlib.sha256).digest()
return hmac.compare_digest(digest, value)
A production verifier must atomically decrement attempts and consume success in shared storage. Otherwise concurrent requests can each observe the same state. Bind the challenge to the login transaction, not merely a phone number.
Consider the awkward sequence, because it exposes the ownership problem better than a happy-path diagram. A passenger requests a login code, the first transport attempt times out without a definitive delivery result, and the worker schedules a retry. Ten seconds later the passenger taps resend. If the public endpoint, queue worker, and verifier each invent their own policy, the system may create two challenges, deliver them out of order, and reject the newest-looking message because the older database row happens to be current. The cleaner rule is to let the authentication service own challenge state and let the transport own delivery attempts only. The resend operation consults the current challenge under a lock, applies the cooldown, and either keeps that challenge or atomically replaces it according to one documented policy. The worker can retry delivery, but it cannot extend expiry or create a code. The verifier reads the same authoritative record. This costs a little more state-machine work up front; it buys behavior that can be tested with a fake clock and concurrent requests.
Limit sends by destination, account, risk bucket, and challenge. Return generic responses to reduce account enumeration. During a resend cooldown, either resend the current challenge through a controlled path or revoke it and create exactly one replacement. Do not leave several codes valid.
Retry transient delivery failures with exponential backoff and jitter, stopping when the code would arrive near expiry. Permanent destination errors end immediately. The client cooldown controls demand; worker retries repair one delivery. They are different clocks.
Isolate disruption traffic from authentication
An airline disruption can fan out to many passengers at once. Alerts, OTPs, and receipts should have separate queues and concurrency budgets even when they share a renderer. Alert retries must not consume a passenger's OTP resend allowance, and a large alert backlog must not take all authentication capacity.
Use business-specific deduplication keys. A receipt keys on its settled-payment event; an alert can key on itinerary, disruption revision, and channel; an OTP keys on its challenge. A generic message identifier hides these distinct guarantees.
An accepted handoff does not mean a person read the message. Model queued, handed-off, delivered when supported, permanently failed, and expired as different states. For fallback, define consent and channel policy beforehand. SMS can carry a short urgent notice while email carries detail, but spraying both channels on every retry creates duplicates. The application should remain the authoritative source of current itinerary status.
Measure before copying the design
Replay one payment event 100 times in a test harness and assert one logical delivery intent. Freeze time around OTP expiry, race verification requests, and assert that one request at most consumes the challenge. Enqueue a synthetic alert fan-out beside login challenges and verify reserved authentication capacity. These are evaluation inputs, not production benchmark claims.
Track queue age per message class, render failures per template version, retry reasons, challenges expired before send, verification success by challenge age, and idempotency suppression. Do not put codes, addresses, phone numbers, or message bodies in metric labels.
Before adopting this boundary, measure event redelivery, channel latency across useful percentiles, and who must approve copy. Those observations determine idempotency retention, OTP lifetime, queue isolation, and template ownership. A provider feature list cannot decide them.
Further reading
- NIST SP 800-63B, Digital Identity Guidelines: https://pages.nist.gov/800-63-3/sp800-63b.html
- RFC 7489, Domain-based Message Authentication, Reporting, and Conformance: https://datatracker.ietf.org/doc/html/rfc7489
Top comments (0)