Skip to content

Build1 publisher3 min readPublished

A bounce-safe welcome path makes the outbox message ID the transport idempotency key

A dev.to post argues that the email API versus SMTP question follows from where suppression lookups, message IDs and delivery-event state live, and its Go sketch leaves the transport as the only interface the decision touches.

The Engineer · Build desk

Illustration accompanying A bounce-safe welcome path makes the outbox message ID the transport idempotency key

What happened

  • A dev.to post says the backend route to prefer is the one that can check suppression before a send, attach its own stable message ID, and turn delivery events into a single idempotent state transition.
  • Its deciding test between an email API and SMTP is whether the same application message ID survives from enqueue through event ingestion, and whether a terminal failure can be classified without scraping prose.
  • The accompanying Go send path renders locally, checks suppression before submission, and passes the outbox message ID to the transport as the idempotency key.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • cost The application pays these invariants per message, since no transport holds that state on its behalf: a stored ID you generated, a recorded content version, and one state transition per delivery event.
  • constraint SMTP clears the second test only for teams that already operate a relay with mature message construction and event normalization. That limits "just use SMTP" to shops that have already built that layer.
  • decision Any team that wants non-developers editing copy has to add per-attempt storage of the remote template identifier and version first. That is a schema and write-path change, not a provider setting.
  • capability If the provider echoes your message ID back on delivery events, a hard bounce joins straight to the player row, and on-call work stops depending on a timestamp search in someone else's dashboard.

Deliver takes a Message and three interfaces. Suppressions.Contains looks up a recipient, and Renderer.Welcome turns a template version and a player ID into a subject and a body. Transport.Send takes an idempotency key, a recipient, a subject and a body [5]. Swapping an SMTP relay for an HTTP API rewrites one of those three implementations, because the eligibility check and the key are chosen by the caller [21].

Message carries ID, PlayerID, Recipient, Region and TemplateVersion [4]. One field addresses the mail. The other four exist to join the attempt back to a player, route it to a permitted endpoint, and record which content version went out [20].

The published listing stops mid-line inside the error branch of the suppression lookup [6]. Fail-open versus fail-closed is left to the reader. The code does define a distinct ErrSuppressed sentinel [19], so a blocked recipient and a broken suppression store return differently to the caller [23]. That ordering also makes the store a dependency of every send.

The second half of the author's deciding test is the harder one: classifying a terminal failure without scraping prose [3]. That constrains the provider. The post notes that an email API usually exposes a structured request and structured delivery events, while SMTP offers a widely understood submission boundary that can fit an existing relay, and says neither statement produces a universal winner [15]. The author is blunt about the standard: "a dashboard showing delivery activity is not a control plane" [8]. And on transports: "I would reject either route if its failure state cannot be joined back to the player and message without searching a dashboard by timestamp" [17].

Provider-rendered templates move that boundary and add a write per attempt. The application has to record the remote template identifier and version used for every attempt, or a replayed job can send different content from the original and incident reconstruction becomes guesswork [11]. The post calls that a valid trade when independent editing is required and poor default behavior when engineers own both copy and delivery [12].

Region gets the same rule. Keep one logical template version, pick the permitted regional endpoint at runtime, and store only the event fields needed to enforce delivery state [13]. An SMS fallback in the United States is a separate regulated channel; the post points to the A2P 10DLC documentation for registration requirements on application-to-person traffic over ten-digit long codes, and says not to turn an email bounce silently into an SMS send [14].

The post does not include deliverability numbers or a named provider [22]. The argument rests on the ordering in Deliver and on the question the author asks first: what page fired when a hard bounce arrived, and did any later welcome retry get stopped [9].

What to watch

  • Whether the author publishes the rest of Deliver, including the branch that runs when the suppression lookup itself fails.
  • Whether the same ownership rule survives an SMS fallback, where 10DLC registration state has to be checked before a send.
  • A provider-by-provider comparison of bounce classification fields would test whether the application message ID actually survives event ingestion.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories