When an application asks an email API to send a password-reset link, receipt, or alert, the user sees a simple result: the request was accepted or rejected.
Behind that response, however, several systems still have work to do. The API must store the request, place work on a queue, let a worker call an email provider, and record what happened. A failure between any two of those steps can leave the application believing that an email is on its way when no worker knows about it.
I am building HayaSend, an Apache-2.0, Resend-compatible transactional email platform. An application can use the official Resend Node SDK and point its baseUrl at HayaSend. The main difference is operational: the delivery provider and data plane remain in the user's cloud account.
The purpose of the work in this article was specific:
After HayaSend accepts an email request, preserve enough durable information to recover the work without asking the application to send the request again.
This article follows one email request through that recovery path. I will introduce each moving part before discussing the AWS and DynamoDB details.
HayaSend is still early beta. The evidence here covers one tested AWS recovery path, not the production readiness of the whole platform.
The system in one diagram
The normal path looks like this:
Application
| Resend-compatible API request
v
HayaSend API
| stores the accepted message and recovery intent
v
DynamoDB <--- dispatcher looks for unpublished work
| |
| v
| Queue
| |
| v
+---------------> Worker ---> Email provider ---> Recipient
The terms used below mean:
- API: the endpoint the application calls to request an email.
- Database: the durable record of the message, recipients, and delivery state. HayaSend's tested AWS path uses DynamoDB.
- Queue: a buffer of jobs waiting for a worker. It lets the API return without waiting for the provider to finish.
- Worker: code that takes a queued job and continues delivery.
- Provider: the external service that ultimately accepts the email for delivery.
- Dispatcher: HayaSend code that finds durable work in DynamoDB and publishes it to the queue.
The database answers “what did we accept?” The queue answers “what should a worker do next?” The reliability problem appears when those answers get out of sync.
The failure I wanted to prevent
A straightforward implementation performs two separate operations:
1. Save the email request in DynamoDB
2. Publish a send job to the queue
Now imagine a password-reset request:
- DynamoDB successfully stores the message.
- The API process stops before queue publication finishes.
- The application has already received an accepted response, so it does not retry.
- The database contains the request, but the queue contains no job.
- No worker sends the reset email.
This is a dual-write problem: one logical operation depends on successful writes to two different systems.
A client idempotency key solves a related but different problem. It can stop repeated API requests from creating several logical messages. It cannot repair work when the application never repeats the request.
The pattern is established; the implementation choices are not
I did not invent the solution pattern. AWS Prescriptive Guidance already documents the transactional outbox pattern, including the dual-write problem, possible duplicate messages, ordering, and the need for idempotent consumers.
An outbox is a durable “work still needs to be published” record stored with the business data. Instead of trying to update DynamoDB and the queue as one impossible cross-service transaction, the API commits the message and its outbox item together in DynamoDB. A dispatcher can publish the queue job later.
The pattern gave me the starting point. It did not decide:
- which HayaSend records must be committed together;
- whether the public recipient limit fits in one DynamoDB transaction;
- how repeat publications keep the same logical identity;
- how multiple dispatchers compete safely for work;
- what to record when an email provider may have accepted a request;
- how to observe recovery without exposing email data.
Those are the implementation decisions I made and tested.
Step 1: make “accepted” mean “recoverable”
When HayaSend accepts a delivery, one DynamoDB TransactWriteItems operation writes:
- one message metadata record;
- one provider-neutral delivery record;
- one record per recipient;
- one optional idempotency claim;
- one initial outbox item;
- one durable backlog counter update.
These records either commit together or do not become visible at all.
For a request with HayaSend's public maximum of 50 recipients, a focused v0.3.11 test observed 55 transaction actions:
1 message + 1 delivery + 50 recipients + 1 idempotency claim
+ 1 outbox item + 1 backlog counter = 55 actions
Here, an “action” is one item-level operation inside the DynamoDB transaction. AWS currently allows up to 100 actions in TransactWriteItems, as documented in DynamoDB constraints and the TransactWriteItems API reference.
The limit itself is ordinary AWS knowledge. The useful project result was checking the real HayaSend record model at the real API limit, including the five non-recipient actions that are easy to forget.
This changes the meaning of API acceptance. It still does not mean “the recipient received the email.” It means “the message and the intent to continue delivery were durably committed together.”
Step 2: recover without replaying the API request
After the transaction commits, the dispatcher searches for due outbox items and publishes queue jobs.
If the API process stops before publication, the outbox item remains in DynamoDB. A later dispatcher can find it. The application does not need to repeat the original send request, and the API does not need to recreate the message from logs.
A simplified recovery sequence is:
API commits message + outbox item
|
X process stops before queue publication
Later dispatcher finds the outbox item
|
v
publishes queue job
|
v
marks outbox item dispatched
The durable source of truth is the outbox item. A scheduled trigger can wake the dispatcher, but the schedule itself is not proof that the work still exists.
Step 3: give repeated publications one logical identity
The dispatcher can also fail after the queue has accepted a job but before DynamoDB records the successful publication:
1. Dispatcher acquires the outbox item
2. Queue accepts the job
3. Dispatcher stops before acknowledging the outbox item
4. The lease expires
5. Another dispatcher publishes the item again
The queue may therefore contain a duplicate. The transactional outbox does not provide exactly-once delivery.
HayaSend derives the job ID from the message, job type, and generation instead of creating a new random ID for each publication attempt. Both queue messages then describe the same logical job. Downstream code can use that identity in deduplication and conditional state changes.
This is a deliberately limited guarantee: duplicates may exist, but retries keep the same name.
Step 4: use an index to find work, not to decide ownership
Several dispatchers may run at the same time. They need to avoid treating the same outbox item as exclusively theirs.
HayaSend uses a DynamoDB global secondary index (GSI) to find items whose due time or lease expiry has passed. A GSI is an alternate lookup view of the table, but its contents can briefly lag behind the base table. AWS explains in its read consistency guide that GSI reads are eventually consistent.
For that reason, an index result is only a candidate. It does not grant ownership.
The dispatcher performs a conditional update on the base-table item to acquire a short-lived lease—a claim that says, in effect, “this dispatcher may work on the item until this time.” If another dispatcher has already changed the item, the conditional update fails.
On publication failure, HayaSend releases the lease and makes the item due again. On success, it acknowledges publication and adjusts the backlog together.
Step 5: admit when the provider result is unknowable
Repairing the database-to-queue boundary reveals another failure window later in the path:
Email provider accepts the request
|
X network response is lost
|
HayaSend cannot confirm the result locally
Automatically retrying may produce a duplicate email. Reporting success may hide a lost request. Reporting an ordinary failure incorrectly claims that the provider rejected it.
HayaSend records this result as ambiguous: the provider may have accepted the request, but the local system could not confirm and commit the outcome.
The outbox cannot remove this uncertainty. Recovery depends on the provider's own idempotency or correlation features. The important design choice was to preserve the uncertainty instead of converting it into a more convenient but unsupported answer.
Step 6: observe recovery without exposing email content
Operators need to know whether delivery work is stuck. They should not need recipient addresses, subject lines, or message bodies to answer that question.
HayaSend's default outbox diagnostics expose aggregate operational facts such as:
- counts of due, leased, expired, and undispatched items;
- the age of the oldest due item;
- cumulative publication failures;
- whether a bounded diagnostic query was truncated.
They exclude addresses, subjects, bodies, raw provider responses, queue endpoints, credentials, and signed URLs.
Recipient and attempt IDs are random opaque values rather than values derived from an email address. Provider events use an opaque event ID or a digest of normalized allowlisted fields instead of copying the raw provider payload.
The goal is not zero observability. It is enough observability to operate the recovery loop without making logs and metrics another store of customer email data.
What I actually tested on AWS
I checked the v0.3.11 implementation at three levels:
| Question | Check | Observed result |
|---|---|---|
| Does the record model fit the transaction limit? | Maximum-recipient focused test | 50 recipients produced 55 actions |
| Do the model, outbox, DynamoDB adapter, and workflow agree? | Six focused test files | 43 tests passed |
| Can a committed item be recovered in a deployed AWS stack? | Public integration workflow | Outbox recovery and acknowledgement succeeded |
The deployed evidence is GitHub Actions run 30498672002.
In its recovery step, the workflow:
- used a delivery already created by the API probe;
- made that delivery's durable outbox item due;
- invoked the deployed dispatcher;
- required the item to gain
dispatched_at; - required its lease and pending-index fields to be removed.
The workflow did not replay the client's API request and was designed not to send an actual email through Amazon SES. It deployed an ephemeral stack in a dedicated account, used GitHub OIDC instead of a long-lived AWS access key, and removed the test resources afterward.
The run used commit 071c2a3, an ancestor of v0.3.11. I compared the run commit with the release tag: the relevant outbox implementation, focused tests, and recovery step were unchanged; the workflow changes were tool-version pins.
This evidence supports one narrow claim: after an API-created delivery was committed, the deployed AWS dispatcher could recover and acknowledge its outbox work without another client request.
It does not prove that every email is delivered, that every provider behaves identically, that every deployment pack has the same evidence, or that an early-beta platform is production-ready.
The practical takeaway
AWS's transactional outbox guidance answered the architectural question: how can database state and later queue publication survive a crash between systems?
Implementing it for an email API required more concrete answers:
- Define exactly what “accepted” guarantees.
- Count every transaction action at the public API limit.
- Preserve one logical job identity across publication retries.
- Let a conditional base-table write—not an eventually consistent index—grant a lease.
- Represent possible provider acceptance as an explicit uncertain state.
- Test the failure path on deployed infrastructure without sending customer email.
- Make recovery observable without copying sensitive payloads.
If you operate a database-to-queue workflow, draw the normal path first, then mark every place where one system may have accepted work while the next system has not recorded it. Those gaps are where recovery state and honest guarantees matter most.
Which boundary is least explicit in your system today: API acceptance, queue publication, or provider acknowledgement?
HayaSend's provider-neutral state model is documented in delivery-model.md, and the deployed test is described in aws-integration-testing.md.
Disclosure: I used an AI assistant to organize public source material and edit this article. I reviewed the AWS documentation, HayaSend v0.3.11 implementation and focused tests, generated-contract check, source comparison, and public AWS run described above on August 7, 2026.
Top comments (0)