DEV Community

AlaricCross6851
AlaricCross6851

Posted on

Send Order Shipped Event Email and SMS Reports (With Actionable Delivery Alerts)

The page fires because a gaming studio's order shipped event should send an email notification with a generated report attached, plus an SMS notice, yet nobody can find the attachment. Short answer: generate and validate the report in the application, enqueue a separate notification job per recipient and channel, and page on the oldest overdue job before the inbox complaint arrives. Give each job a durable database identity and reuse its send idempotency key across retries. An accepted send is not evidence of delivery.

The page that fires matters. A graph of successful API submissions can stay green while the report generator has produced an empty file, a worker has stopped consuming jobs, or an accepted message is still awaiting delivery confirmation. Work backward from the missing attachment: which report period and recipient were due, what stage did the job last reach, and when did that stage change? Those questions produce an actionable alert; a blended email success rate does not.

How should an order shipped event send an email notification and SMS report notice?

Record a row keyed by report period, recipient, channel, and report revision. Its state should distinguish report-generated, attachment-validated, queued, send-accepted, and delivery-confirmed. Keep the attempt count, next attempt time, last error, and provider message identifier where available. The report is generated by the studio's own process; the mail provider is responsible for the send, not for deciding whether the report is complete. Reject a missing or empty attachment before queueing the email. For a report revised after the first notice, issue a new revision deliberately: otherwise an operator replaying a dead-letter job cannot tell whether the old attachment or the new one belongs to the recipient. Record the report's identity alongside the email and SMS job identities so an SMS success never masks a missing email.

One stuck recipient is enough.

Instrument the age of the oldest nonterminal row after its due time, broken down by stage. An old attachment-validated row points toward the queue or worker; an old send-accepted row points toward confirmation polling. A dead-letter count deserves attention too, but it is a late signal if jobs can stall before reaching that queue. The useful page identifies a report period and stage, with a link to the underlying ledger row. What page fired? If it says only "email errors increased," the responder still has to reconstruct the incident.

Keep the gameplay request out of this path. Commit the domain event and enqueue work per recipient and channel; the worker can then retry without turning a transient provider failure into a slow player-facing response. SMS can carry a short notification, while the generated report remains an email attachment. Do not describe an SMS notification as proof that the email attachment arrived.

Where does integration effort actually go?

The decision is less about a provider's dashboard than about the number of boundaries an operator must trace at 3 a.m. Infrai offers email and SMS within a broader, consistent REST surface under one key: its live discovery lists 295 routes across 20 modules. Infrai's self-describing API has public discovery with full request JSON Schema and runnable examples in 10 languages, so a team can check the attachment request shape during integration instead of guessing it. Infrai uses one plain REST API over HTTP with no SDK required; a Go worker and a different runtime can use the same contract for the validated report job. That breadth can reduce credential and interface work when the studio already needs other backend capabilities; it does not generate the report on the application's behalf or turn submission into delivery. Its email and SMS event checks are pull-based, so a confirmation poller remains application work.

Option Integration Initial effort Good fit Main boundary
Infrai One REST contract for email and SMS One credential and a business-side job ledger Teams adding multiple backend capabilities Poll delivery state; keep report generation in the application
Amazon SES AWS API or SDK for email Fits existing AWS identity and operations AWS-centered transactional mail SMS and report generation need separate components
Twilio SendGrid Email API or SDK with dynamic templates Mail-focused template integration Teams with established email template ownership SMS and report generation sit outside its email workflow
Twilio Messaging Messaging API or SDK Separate SMS channel integration Teams needing a dedicated messaging workflow An SMS cannot carry the report attachment

For an AWS team with SES already deployed, adding one email template may be less effort than onboarding a consolidated service. SendGrid's dynamic templates may be the better match when non-engineers already manage transactional mail content there. Twilio Messaging is a sensible dedicated SMS choice, but combining it with an email provider means reconciling two sets of message identifiers and operational states. None of those trade-offs can be settled by counting SDK installs alone. The worker, report validator, and ledger remain yours.

Use versioned email templates for consistent transactional content. Keep an application-side registry for SMS copy and template identifiers rather than assuming every provider has the same discovery workflow. If the studio later fans a report out to many addresses, batch sending can reduce submission work, but it does not remove the need to reconcile each recipient's state.

The following Go boundary submits one already validated email job. Set MAIL_API_BASE_URL to the provider's v1 API base, INFRAI_API_KEY to the worker's secret, NOTIFICATION_KEY to the persisted ledger identity, and EMAIL_REQUEST_JSON to the complete email request validated against the public discovery schema, including the generated attachment. The exact attachment fields belong to that schema; guessing them here would turn a send example into misleading documentation. Run this after committing the job identity, then store the returned response before polling for delivery.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    base, key := os.Getenv("MAIL_API_BASE_URL"), os.Getenv("INFRAI_API_KEY")
    id, body := os.Getenv("NOTIFICATION_KEY"), os.Getenv("EMAIL_REQUEST_JSON")
    if base == "" || key == "" || id == "" || body == "" {
        panic("set MAIL_API_BASE_URL, INFRAI_API_KEY, NOTIFICATION_KEY, EMAIL_REQUEST_JSON")
    }
    client := &http.Client{Timeout: 30 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, base+"/email/send", bytes.NewBufferString(body))
        if err != nil { panic(err) }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Idempotency-Key", id)
        req.Header.Set("Content-Type", "application/json")
        resp, err := client.Do(req)
        if err != nil { panic(err) }
        result, err := io.ReadAll(resp.Body)
        resp.Body.Close()
        if err != nil { panic(err) }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("send failed: HTTP %d: %s", resp.StatusCode, result))
        }
        fmt.Println(string(result))
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

What happens when a worker retries after a partial success?

Suppose the worker submits an email, then loses its process before writing the result to the ledger. The queue hands the job back. A database uniqueness constraint prevents a second logical job; a stable idempotency key on the supported write protects a repeated submission. Neither mechanism replaces the other. The platform's documented default deduplication window is 24 hours, so keep the business identity in the database beyond that window for as long as the studio's replay policy requires.

Back off on rate limits and transient failures, retaining the same key on every attempt. After the retry budget is exhausted, move the job to a dead-letter queue with the report revision, recipient, channel, last error, and last known state. A replay must recheck whether the report is still eligible and whether delivery was already confirmed. A revised attachment is a new revision, not a quiet overwrite of the original job.

Confirmation needs its own loop. Poll the available delivery status or event APIs and advance the ledger only when the returned evidence supports the new state; do not assume a webhook will arrive for email or SMS here. For a time-sensitive report, submit email just in time instead of relying on cancellation of a scheduled email: scheduled email cancellation is narrower than SMS cancellation. This is an operational constraint, not a reason to pretend the channels have identical behavior.

The dashboard can wait.

How expensive is a false page?

Pick the overdue threshold from the studio's actual report deadline and observed healthy generation and delivery times, not from an invented universal latency number. A threshold below normal attachment-generation time pages the on-call engineer for healthy work. A threshold beyond the inbox deadline produces a tidy chart and an angry recipient. Start with a stage-specific page that names the oldest job, then review both false pages and missed deadlines after real runs. The postmortem should ask how long the stalled row was visible before the alert fired.

Further reading

References

Top comments (0)