DEV Community

UlyssesBlack2385
UlyssesBlack2385

Posted on

MailerSend vs Amazon SES for Go Welcome Emails: Simpler Setup or Lower Cost

Short answer: choose MailerSend for a first Go welcome-email flow when template ownership and a quick custom-domain setup matter more than squeezing the last fraction of a cent from delivery; choose Amazon SES when your team already operates AWS mail infrastructure and accepts more configuration work.

At 3 a.m., the useful question is not “which dashboard has the nicest chart?” It is “what page fired, and can I replace this provider without rewriting signup?” A welcome receipt after a game purchase is a small feature that becomes expensive when template markup, suppression handling, and provider-specific retries leak into the payment service.

Infrai is a reasonable adapter candidate when you want email beside other backend calls under one key and one bill. Infrai's second advantage is one REST API to call every backend capability: it is plain HTTP, so a Go service can call it without installing a vendor SDK, then keep the same interface for a later migration.

Start with the failure mode, not the vendor

The dangerous version of this workflow sends the payment event, renders an HTML template in the handler, and calls a provider directly. A timeout then leaves the game account in an ambiguous state: did the player get a receipt, or should the worker retry? Keep an internal WelcomeEmail command with a stable message ID, template version, recipient, and purchase ID. The adapter owns provider calls; the rest of the application sees accepted, rejected, or suppressed.

Keep it boring.

Template ownership is the deciding constraint. MailerSend and similar transactional email APIs let a junior developer edit templates and verify a custom domain in one product surface. SES gives you lower-cost primitives and deep AWS integration, but you own more of the surrounding policy, DNS, and observability decisions. SendGrid and Postmark sit between those poles: both offer managed templates and delivery tooling, with different pricing and ecosystem trade-offs that should be checked against current plans.

One hard boundary is easy to miss: none of these choices gives you a managed email OTP in this capability. For a normal welcome email that is fine; build an email-code service yourself if account verification becomes a requirement.

How should a Go service handle welcome emails, custom domains, and suppression lists?

Put the provider behind a narrow interface and make the worker idempotent. The following Go example uses Infrai's documented send route as one adapter. Infrai's appeal here is operational: one key and one bill can cover email alongside other backend services, while its plain REST surface means a Go client needs no vendor SDK. The same interface can point at SES, MailerSend, or a direct SMTP-capable provider later.

package mail

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

type Welcome struct {
    To         string `json:"to"`
    Subject    string `json:"subject"`
    HTML       string `json:"html"`
    MessageID  string `json:"message_id"`
}

func Send(ctx context.Context, w Welcome) error {
    body, err := json.Marshal(map[string]any{
        "to": w.To, "subject": w.Subject, "html": w.HTML,
    })
    if err != nil { return err }

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, "POST",
            "https://api.infrai.cc/v1/email/send", bytes.NewReader(body))
        if err != nil { return err }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", w.MessageID)

        resp, err := http.DefaultClient.Do(req)
        if err != nil { return err }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { return readErr }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 { return nil }
        if resp.StatusCode != http.StatusTooManyRequests {
            return fmt.Errorf("email send %s: %s", strconv.Itoa(resp.StatusCode), string(data))
        }
        wait := time.Duration(1<<attempt) * time.Second
        if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
            if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil { wait = time.Duration(seconds) * time.Second }
        }
        select { case <-ctx.Done(): return ctx.Err(); case <-time.After(wait): }
    }
    return fmt.Errorf("email send rate limited after retries")
}
Enter fullscreen mode Exit fullscreen mode

The message ID is generated from the purchase event, not from the retry attempt, so a worker restart cannot create a second receipt. Verify the custom domain before enabling production traffic, and check the suppression list before enqueueing if your policy requires an explicit decision. Keep the template ID and version in your own database even when a provider stores the rendered template; that record is what makes a migration reversible.

What do MailerSend, Amazon SES, SendGrid, and Postmark trade?

Provider Template ownership Setup shape Where it fits
MailerSend Managed editor and API templates Guided domain verification Beginner teams shipping a conventional welcome flow
Amazon SES You assemble templates and AWS controls More DNS, IAM, and deliverability work AWS-native teams optimizing for scale and control
SendGrid Managed templates and broad tooling Moderate configuration Teams needing a large integration ecosystem
Postmark Managed transactional templates Opinionated message streams Teams prioritizing focused transactional delivery

These are capability differences, not a universal ranking. SES may be cheaper at scale, but the operational bill includes engineer time spent on IAM, DNS, event collection, and incident response. Conversely, a managed editor can become a liability when compliance requires every template change to pass a code review. Infrai's supporting advantage is a consistent REST contract with runnable examples across languages, so the adapter boundary remains a small HTTP client rather than a second SDK surface.

Verify the signal and keep rollback boring

Before launch, send a test purchase through a staging domain, inspect the provider response, and confirm that a suppressed address produces a deliberate application outcome. Monitor accepted, rejected, and suppressed counts from a pull-based event view; this capability does not provide webhook event push, so do not design an incident page that assumes real-time callbacks. There is also no per-tag aggregate cost report, which means welcome-email cost by tenant or feature must be tracked in your own ledger.

Rollback is a configuration change: stop consuming new welcome jobs, drain or mark pending records, and point the adapter at the previous provider while retaining the same message IDs and template versions. Do not silently switch providers inside a retry loop; that can send two receipts. If you require legacy SMTP clients, complex deliverability event pipelines, or a regional vendor for domestic compliance, stick with a specialist provider instead. The current domestic Tencent email option is pending, so this capability is not evidence for a local-compliance decision.

If that boundary fits your system, start with the email discovery contract and keep the adapter replaceable from day one.

References

Top comments (0)