SendDart in Go Applications: Use Context Without Losing Send Evidence

SendDart in Go Applications: Use Context Without Losing Send Evidence

Design Go email workers with context-aware calls, transaction-safe intent, bounded goroutines and explicit recovery after cancellation.

SendDart Team

TL;DR

  • Treat Go context cancellation as local control, not proof the external send never happened; design durable notification records so a canceled context produces a controlled uncertain state rather than a new message.
  • Commit business intent in the database, then have a durable worker perform the external send; avoid calling the provider inside a transaction and use a uniqueness rule to prevent duplicate intended notifications.
  • Bound goroutines and treat shutdown or cancellations as recovery events; verify with tests that check database state and external attempt counts, not just err == nil on a single call.

Context controls waiting, not remote history

Go's context model helps applications coordinate deadlines and cancellation, but canceling a local request does not prove that an external email operation never happened. The remote service may have accepted the message before the local caller stopped waiting.

Design the notification record so it survives that distinction. Store business intent, operation identity and the last known attempt stage outside the goroutine performing the call. A canceled context can then lead to a controlled uncertain state rather than an automatic new message.

This is especially important when sending is started from an HTTP handler. The client's connection may end while the business notification remains valid. Decide whether the notification belongs to a durable worker or to the request lifetime instead of inheriting that choice accidentally.

Related reading: Scheduled Email Delivery: Store the Time, Content and Cancellation State.

Commit intent with the business operation

A Go service can use a database transaction to record the business event and its notification intent together. Follow the database driver's transaction contract and avoid mixing transactional and non-transactional calls unintentionally. The Go documentation explains the role of sql.Tx for coordinated operations. Go database transactions.

Do not call the provider while holding a transaction open and assume rollback can undo the email. The external operation is outside that database's atomic boundary. A durable worker should perform it after the intended event commits.

Use a uniqueness rule for business event and message purpose. Concurrent handlers processing the same upstream event should create one intended notification. The provider operation key then protects supported recovery of that notification; it does not replace the business uniqueness rule.

Related reading: Best Email API: Choose With a Production Acceptance Test.

Use the SDK's context-aware contract

SendDart's Go SDK documents context-aware variants and typed error information. Review the actual methods and request options for your operation rather than assuming every resource shares the same signature. SendDart Go SDK.

Pass a context with an intentional deadline that leaves time to persist the result. The worker's lease and shutdown deadline should be coordinated with the client call. A request that runs until the process is forcibly terminated cannot reliably record what it learned.

Inspect typed provider errors using the documented Go error model. Preserve the status, name and progress fields that influence recovery. A plain error string is insufficient to distinguish a malformed request from an uncertain handoff.

Bound goroutines and outstanding work

Launching one goroutine per notification in an unbounded result set can overload the process, database and provider. Use a bounded worker pool or another deliberate concurrency mechanism. Claim a limited set of jobs and record each result independently.

Backpressure should be visible. Track pending age and available worker capacity rather than allowing a large in-memory channel to hide a durable backlog. A channel can coordinate goroutines, but it is not a replacement for persistent notification state.

Apply fairness when multiple tenants or message classes share workers. A large import should not necessarily block time-sensitive account messages. Provider rate limits and application scheduling are separate constraints, and both need a policy.

Preserve identity across cancellation and retry

Assign the intended operation key before submission. For supported SendDart send operations, pass that stable key through the documented request options and retain the same payload during recovery.

If the call is interrupted, record any available provider ID and avoid immediately creating a new key. Reconcile the original operation according to the service contract. A canceled local context is not equivalent to a confirmed pre-processing rejection.

For a partial batch, retain item-level evidence. Confirmed items, attempted items and never-attempted items need different treatment. Do not rebuild the entire batch under a fresh key because one goroutine returned an error.

Separate immutable content from current policy

Render from a fixed template version and event snapshot. A retry after a deployment should not change an invoice amount or invitation destination silently. If content must change, create a new explicitly authorized operation.

Recheck suppression, tenant status and sender ownership just before submission. These policies may change while the job waits. Record a skipped outcome when current policy blocks sending, rather than treating it as a provider outage.

Keep secrets and sensitive token values out of structured logs. Include stable internal and provider identifiers instead. A support investigation should not require printing the entire request payload from a failed worker.

Handle shutdown as a recovery event

On shutdown, stop claiming new jobs and allow bounded completion according to your service design. Work still in progress may remain uncertain when the process exits. The next worker must be able to distinguish that state from a job never submitted.

Do not reset every expired claim to pending without checking attempt evidence. Lease recovery and provider recovery are related but separate. A crash can release ownership while leaving a remote operation active.

Test this with a controlled transport that blocks after recording a submission, then cancels the context. Assert that the application preserves identity and does not blindly issue a new send when another worker takes over.

Prove the complete Go workflow

Test transaction rollback, competing workers, context expiry, shutdown and duplicate callback processing. Verify both database state and external attempt count. A test that only checks err == nil on one call misses the most important boundaries.

Use a small simulator or controlled live canary to confirm actual SDK configuration and event correlation. Keep that evidence separate from tests using fake HTTP responses.

A strong Go integration uses context to control local execution while durable records preserve remote uncertainty. Bounded goroutines, explicit transactions and typed recovery make the notification system understandable under load and interruption.