A welcome message is allowed to be late; it is not allowed to keep targeting an address already known to be invalid. That operational constraint changes the transport choice: keep template ownership and recipient eligibility in the application boundary, then choose an email API when structured event ingestion matters more than protocol portability, or SMTP when a narrow, established send path is the more valuable constraint.
TL;DR: neither transport fixes bounce handling. The easiest backend route is the one that can check suppression before a send, attach your own stable message ID, and turn delivery events into one idempotent state transition. For a game, the template belongs with the player lifecycle code; provider-rendered templates are reasonable only when non-developers must change content independently and the team accepts the resulting deployment and recovery boundary.
I carry a pager, and I have been woken by alerts that meant nothing and missed the one that mattered. The postmortem lesson is blunt: a dashboard showing delivery activity is not a control plane. I ask a different question first: what page fired when a hard bounce arrived, and did any later welcome retry get stopped?
That is the boundary.
Failure analysis: template ownership decides recovery
Template ownership is more consequential than the HTTP-versus-SMTP argument. If the game backend owns player creation, locale selection, consent state, and the decision to suppress an address, keeping the subject and body version beside that logic gives one reviewable release unit. The transport receives rendered content and metadata; it does not decide who should be contacted.
Remote templates move that boundary. They can let an operations or content team revise copy without deploying the game backend, but the application must then record the remote template identifier and version used for every attempt. Otherwise, replaying a failed job may send different content from the original attempt, and incident reconstruction becomes guesswork. That is a valid trade when independent editing is required. It is poor default behavior when engineers own both copy and delivery.
The same ownership rule applies across US and EU routes: region is routing and data-handling configuration, not an excuse to fork business meaning. Keep a single logical template version, choose the permitted regional endpoint at runtime, and store only the event fields needed to enforce delivery state. If SMS is added as a fallback in the United States, treat it as a separate regulated channel; the US A2P 10DLC documentation describes registration requirements for application-to-person traffic over ten-digit long codes. Do not silently turn an email bounce into an SMS send.
Should a welcome app use a transactional email API or SMTP?
An email API usually exposes a structured request and structured delivery events. SMTP offers a widely understood submission boundary and can fit an existing mail relay. Those statements do not produce a universal winner. The deciding test is whether the backend can preserve the same application message ID from enqueue through event ingestion and whether it can classify a terminal failure without scraping prose.
For this workload, compare the boundaries rather than feature lists:
| Concern | API-shaped transport | SMTP-shaped transport | Application invariant |
|---|---|---|---|
| Template payload | Rendered body or remote template reference | Rendered MIME message | Record the exact template version |
| Correlation | Application ID in request metadata | Application ID in a message header | Persist one stable ID before submission |
| Event history | Callback or retrieval interface | Delivery-status and relay records | Normalize into your own event schema |
| Suppression | May exist downstream | Often handled outside submission | Check the application suppression set first |
| Regional route | Endpoint or account selection | Relay selection | Make routing explicit and testable |
SMTP is attractive when a relay is already operated, message construction is mature, and event normalization exists. An API is attractive when the team needs a direct typed boundary for submission and events. I would reject either route if its failure state cannot be joined back to the player and message without searching a dashboard by timestamp. At 3am, that search is already too much ambiguity.
Runbook implementation: stop suppressed welcome retries
The send path below is intentionally small. It renders locally, checks suppression before submission, and uses the outbox message ID as the transport idempotency key. The interfaces leave protocol and storage choices open; the invariant does the work.
package welcome
import (
"context"
"errors"
)
var ErrSuppressed = errors.New("recipient is suppressed")
type Message struct {
ID string
PlayerID string
Recipient string
Region string
TemplateVersion string
}
type Suppressions interface {
Contains(ctx context.Context, recipient string) (bool, error)
}
type Renderer interface {
Welcome(version, playerID string) (subject, body string, err error)
}
type Transport interface {
Send(ctx context.Context, idempotencyKey, recipient, subject, body string) error
}
func Deliver(ctx context.Context, m Message, s Suppressions, r Renderer, t Transport) error {
blocked, err := s.Contains(ctx, m.Recipient)
if err != nil {
return err // Fail closed when recipient eligibility is unknown.
}
if blocked {
return ErrSuppressed
}
subject, body, err := r.Welcome(m.TemplateVersion, m.PlayerID)
if err != nil {
return err
}
return t.Send(ctx, m.ID, m.Recipient, subject, body)
}
There is a trap here: checking suppression and then sending are two separate operations. A bounce can land between them. The durable design serializes changes through an outbox or applies a final suppression check in the worker immediately before submission, while event ingestion remains idempotent. No code sample can make a remote send and a local database commit one atomic transaction, so retries must reuse the message ID and the receiver must tolerate duplicate events.
Retries are where vague ownership becomes an incident.
Event history should answer four questions without consulting a provider screen: which logical message was attempted, which template version produced it, what normalized state it reached, and why the state changed. Preserve the raw event reference for investigation, but page on violations of the invariant, such as a submission attempted after suppression, rather than on every bounce. Bounces are expected input. Re-contact after a terminal suppression decision is the incident.
Migration plan: authenticate the domain, preserve eligibility
A custom domain does not change the API-versus-SMTP decision. DKIM, specified in RFC 6376, associates a domain with a message by means of a cryptographic signature and defines verification behavior. Configure domain authentication for the chosen sending path, but do not treat successful authentication as evidence that a recipient remains valid.
This separation matters during migrations. The domain identity can remain stable while the transport adapter changes; suppression data and application message IDs must move with the application boundary. Test authentication with the real route before shifting traffic, then test a synthetic rejected recipient through the same event-normalization path. The first test protects identity. The second protects behavior.
Keep the rollout boring: shadow-normalize events before using them for suppression, compare terminal classifications, move a bounded slice of welcome traffic, and retain a rollback path that does not roll back the suppression set. A template revision should be deployable without resetting delivery state. A transport revision should be deployable without changing the template.
Governance limits and exceptions
Local template ownership is not always correct. Its limitation is organizational: every copy correction follows the software release path, so a legally reviewed message that must be edited and approved outside that path may belong in a controlled remote template system. This is a real trade-off, not a reason to hide the boundary. A legacy estate with a dependable relay and normalized delivery-status pipeline may gain nothing from replacing SMTP, while a channel team that already operates shared suppression and event services may reasonably keep those concerns out of the game backend. The local approach is also unsuitable when the backend cannot retain the exact rendered artifact or template version needed to explain a historical send.
The condition is explicit ownership, not a favored protocol. Document who can change content, who classifies a bounce, where suppression becomes authoritative, how US and EU routing is selected, and which alert fires when those rules are violated. If those answers survive a transport outage and a template rollback, the backend route is easy enough.
Top comments (0)