Guide

A template turns a template ID, a locale and some data into the text a user reads. Bad templates do real damage. A missing variable can send "Hello {{name}}" to millions of people, and one bad deploy can dead-letter millions of jobs. Templates need typed variables, locale fallbacks, version pins and escaping, and a clear failure when something is missing.

The big idea

Treat templates as versioned data, not as code strings scattered through services. Each send pins the template version it was created with. That way, an edit in the middle of a campaign doesn't silently change messages that are already queued. Transactional templates change quickly, and they still carry version pins.

Walking through it

Each send is a typed request. This is a sketch of the contract:

TypeScript
type NotifyRequest = {
  userId: string
  templateId: string
  locale?: string
  data: Record<string, string | number>
  channels?: Array<"push" | "email" | "sms" | "inbox">
  priority: "security" | "transactional" | "social" | "marketing"
  idempotencyKey: string
  scheduleAt?: string // ISO time
}

The worker handles two cases that matter most: a locale with no translation, and a variable with no value. The snippet below runs as written:

TypeScript
// Try the exact locale, then its language, then English
const fallbackChain = (locale: string): string => {
  const [language] = locale.split("-")
  return [...new Set([locale, language, "en"])].join(" -> ")
}
console.log(fallbackChain("pt-BR"))

// Fill {{placeholders}}. A missing value fails the send instead of leaking the placeholder.
function render(template: string, data: Record<string, string>): string {
  return template.replace(/\{\{(\w+)\}\}/g, (_, key: string) => {
    if (!(key in data)) throw new Error(`missing variable: ${key}`)
    return data[key]
  })
}
console.log(render("Your order {{orderId}} shipped", { orderId: "881" }))

try {
  render("Hello {{name}}", {})
} catch (err) {
  console.log("DLQ:", (err as Error).message)
}

Output:

pt-BR -> pt -> en
Your order 881 shipped
DLQ: missing variable: name

The fallback chain avoids a blank push when a translation is missing. The missing name fails the send into the dead-letter queue with a clear message. The alternative is a user who reads "Hello {{name}}", which is a visible bug with no alert attached.

Other rules that follow from this:

  • Validate required variables at ingress against the template's schema, and check again at render time. Two checks are better than one.
  • Escape everything in email HTML. Unescaped data in an email is a real cross-site scripting risk.
  • Respect payload limits. Push payloads are limited to about 4 KB, so keep the text short.
  • Approve marketing templates through a review workflow. Transactional templates need faster changes, but they still need version pins.

Common mistakes

  • Falling back to a raw placeholder. The user sees the template syntax. Fail the send instead.
  • Editing a template mid-campaign. Queued messages then change in unpredictable ways. Pin the version.
  • Putting unescaped user data in email HTML. It opens an injection path.
  • No locale fallback. Missing translations become blank pushes.

Check yourself

  1. Why does a missing variable go to the DLQ instead of being sent?

    A sent placeholder is a visible bug for the user, while a failed send is visible to operators and can be fixed and replayed.

  2. Why pin a template version on each send?

    Edits during a campaign would otherwise change messages already in the queue.

Key takeaways

  • Templates are versioned data, pinned on each send.
  • Use typed variables, and fail the send when a value is missing.
  • Fall back through locales, so a missing translation doesn't produce a blank message.
  • Escape everything in HTML email, and keep push payloads within size limits.

Further learning

Amazon Builders' Library essays on queues and retries apply directly to the rendering and provider work in this pipeline.

Amazon Builders' LibraryA collection of articles that describe how Amazon develops, architects, releases, and operates technology. Use these articles to understand how Amazon builds and maintains systems that are reliable, available, and sec…aws.amazon.com