KAHIBARO
Discord Login Register

18.5. Transactional Emails

Why Transactional Emails Matter

Transactional emails are emails that are triggered by what a user or system does, not by marketing campaigns. They are part of your backend’s core functionality, just like saving to a database or processing a payment.

Typical examples:

If these emails fail, users cannot complete important actions. So as a backend developer you must treat transactional emails as critical infrastructure, not a nice-to-have extra.

Transactional emails are functional emails triggered by a specific event, and users generally expect them. Unlike marketing emails, they should be sent only in direct response to actions or system events.

They often:

Because of this, you should design, implement, and monitor them carefully.

Transactional vs Marketing Emails

It is important to clearly separate these two categories in your codebase and configuration.

AspectTransactional EmailMarketing / Newsletter Email
TriggerUser or system eventBusiness / campaign decision
PurposeComplete a process or provide important infoPromote, sell, or engage
User expectationExpected and often requiredOptional, user may opt in or out
ExamplesPassword reset, receipt, alertsPromotions, product news, weekly digest
Legal treatmentOften allowed even if user unsubscribes from marketingUsually requires explicit consent (opt-in)
FrequencyOccurs only when event happensBroadcast to many users at once

Some rules of thumb:

This separation protects your deliverability. If marketing campaigns get marked as spam, your crucial password reset emails should not start going to spam too.

Common Types of Transactional Emails

Focus on the ones backend services send most often.

Account-related emails

These are required for basic account flows.

Security and access emails

These help keep accounts secure.

Order and billing emails

Very common in e-commerce and SaaS backends.

Activity notifications

These depend on your product but follow similar patterns.

In all cases, an event in your system triggers the email.

Designing Transactional Email Flows

Treat transactional emails as part of your backend’s core workflow. For each important user flow, design:

  1. Trigger
    What exactly causes the email? For example:
    • “User submits password reset form.”
    • “Payment provider sends webhook that a payment succeeded.”
  2. Data needed
    Gather all data needed to build the email without additional queries if possible:
    • Recipient email
    • User name / display name
    • Order data, token, or other specific information
  3. Template selection
    Decide which template to use, for example:
    • password_reset.html and password_reset.txt
    • order_confirmation.html

Many email providers support templates stored on their side and referenced by an ID.

  1. Token or link generation if needed

For flows like password reset or verification, generate secure links of the form:

The backend must validate and store these tokens securely. Details of token creation belong in the authentication chapters, so we only note here that the email contains the token and the backend validates it.

  1. Send strategy

Decide how you will send:

For most real systems, using background jobs is better. We discuss this more below.

  1. Error handling

What if sending fails? Some options:

Design each email type as a small flow with clear triggers, steps, and outcomes.

Implementing Transactional Emails in Your Backend

Most modern backends use a third-party email provider instead of running their own SMTP server. The typical flow looks like this:

  1. Your backend connects to the provider via HTTP API
  2. You call an “send email” endpoint with JSON data
  3. The provider takes care of actual delivery, queuing, and retries

Example: Basic password reset email flow

To show the moving parts, here is a simple Python-style pseudo flow:

  1. User submits their email address to /password-reset/request
  2. Backend:
    • Finds the user by email
    • Generates a secure token (for example a random string, stored with expiration)
    • Builds a reset URL, for example https://app.example.com/reset?token=XYZ
    • Enqueues a background job send_password_reset_email(user_email, reset_url)
  3. Worker process picks up the job and calls the email provider API:
python
def send_password_reset_email(user_email: str, reset_url: str) -> None:
    subject = "Reset your password"
    html_body = f"""
        <p>Hello,</p>
        <p>We received a request to reset your password.</p>
        <p><a href="{reset_url}">Click here to reset your password</a></p>
        <p>This link will expire in 30 minutes.</p>
    """
    text_body = (
        "Hello,\n\n"
        "We received a request to reset your password.\n"
        f"Open this link to reset it: {reset_url}\n\n"
        "This link will expire in 30 minutes.\n"
    )
    email_api.send(
        to=user_email,
        subject=subject,
        html=html_body,
        text=text_body,
    )
  1. When the user clicks the link, the frontend calls an API endpoint, for example /password-reset/confirm, with the token and new password.
  2. Backend validates the token and updates the password.

The key idea for this chapter is that your backend uses transactional emails as part of stateful flows and therefore must handle:

Templates and Personalization

Transactional emails are very repetitive in structure but personalized to each user or event.

Template structure

A simple email template might look like:

html
<!DOCTYPE html>
<html>
  <body>
    <p>Hello {{ user_name }},</p>
    <p>Thank you for your order <strong>#{{ order_id }}</strong>.</p>
    <p>Order summary:</p>
    <ul>
      {% for item in items %}
        <li>{{ item.name }} (x{{ item.quantity }}) - {{ item.price }}</li>
      {% endfor %}
    </ul>
    <p>Total: {{ total }}</p>
  </body>
</html>

In your backend code you then pass data like:

python
context = {
    "user_name": "Alice",
    "order_id": "12345",
    "items": [
        {"name": "Book", "quantity": 1, "price": "$10"},
        {"name": "Pen", "quantity": 2, "price": "$2"},
    ],
    "total": "$14",
}

Your templating engine or email provider fills in the placeholders.

Personalization options

You can safely personalize:

Avoid including:

Keep transactional emails mostly functional and minimal, with clear calls to action.

Sending Emails Reliably and at Scale

In small demos, you might send emails directly inside your HTTP handlers, for example:

python
def register_user():
    # save user to database
    send_welcome_email(user_email)
    return {"message": "Registered"}

For real-world systems this approach has problems:

A better pattern is to treat email sending as a background job.

Synchronous vs asynchronous sending

ApproachDescriptionProsCons
SynchronousSend email inside the HTTP request handlerSimple to implementSlower responses, less reliable
Asynchronous (queue)Enqueue job and let a worker send in the backgroundFast responses, robust retriesRequires job queue / worker infrastructure

In other chapters we cover tools like Celery and message queues. For transactional emails, it is common to:

  1. Enqueue a job with type and payload, for example:
    • job type: send_transactional_email
    • payload: { "template": "password_reset", "to": "...", "data": {...} }
  2. A worker process reads jobs and calls the email provider.

Idempotency concerns

You should think about what happens if the same email job runs twice:

To reduce duplicate sends, you can store an email_events table with:

When sending, you can check if a similar event happened very recently and decide whether to send again. How strict you are depends on the business rules.

Deliverability and Best Practices

Good transactional email design is not only about code. You must also ensure that emails actually reach inboxes.

Technical configuration (high level)

Your DevOps or operations setup should include:

These are DNS and cryptographic settings that tell email providers your emails are legitimate. The exact configuration is usually guided by your email provider.

As a backend developer you often:

Content best practices

For transactional emails:

Remember that:

Never include plain-text passwords or long-lived secrets in transactional emails. Always send links with time-limited tokens, not the actual secret.

Monitoring and logging

You should log:

Log examples:

text
INFO  [email] send password_reset to user_id=42 status=accepted
ERROR [email] send order_confirmation to user_id=7 error="provider timeout"

In larger systems you may also:

This helps debug user complaints like “I did not receive my email.”

Simple Example Flows

To connect everything, here are two compact flows you will likely implement in your projects.

Flow 1: Email verification after registration

  1. User registers with email and password.
  2. Backend:
    • Creates user in database with is_email_verified = False
    • Generates an email verification token with expiration
    • Enqueues send_email_verification(user_email, verification_link)
  3. Worker sends an email:
    • Subject: “Verify your email address”
    • Body: Link like https://app.example.com/verify?token=...
  4. User clicks the link.
  5. Backend validates token, marks is_email_verified = True, and maybe redirects to a welcome page.

Flow 2: Order confirmation

  1. User checks out and pays.
  2. Payment provider calls your webhook with “payment succeeded.”
  3. Backend:
    • Creates an order record
    • Updates inventory
    • Enqueues send_order_confirmation(user_email, order_id)
  4. Worker loads order data from database and sends an email with:
    • Order items
    • Prices and totals
    • Billing and shipping information

Each flow:

This is the core pattern behind nearly all transactional emails in backend systems.

Views: 7

Comments

Please login to add a comment.

Don't have an account? Register now!