Skip to content

DynamoDB

2 min read · main part only; folded sections are optional

After this you can answer: Why does the worker remember which jobs it already did, and why in DynamoDB?

flowchart TB
M["message:<br/>booking confirmed"] --> G{"GetItem<br/>jobKey done?"}
G -->|"yes"| S["skip, report success"]
G -->|"no"| E["send the email (SES)"]
E --> P["PutItem only if<br/>attribute_not_exists(jobKey)"]
  1. Pick a business key. The job key is booking.confirmed:<bookingId>, not the message id. The outbox may publish the same booking twice under two message ids; both map to one key.
  2. Check first. A GetItem with ConsistentRead, so a write a moment ago is seen.
  3. Send, then record. After SES accepts the email, a PutItem with the condition attribute_not_exists(jobKey): of two workers recording at once, exactly one wins.
  4. Forget later. Each item has expiresAt, 30 days ahead. DynamoDB’s TTL (time to live) deletes old items for free, some time after that.

Sending before recording is a choice: a crash in between can send a rare duplicate, but never loses an email. Recording first would lose it. So this is “effectively once”, not exactly once.

TableKeysWritten byRead by
minitickets-dev-processed-jobsjobKeyworker (PutItem)worker (GetItem)
minitickets-dev-ticketsuserId + ticketId; index by-event-datetickets Lambdaapi, Query only

Both use on-demand billing (PAY_PER_REQUEST): no capacity to plan, nothing to pay while idle. The api’s “My tickets” is one Query on the signed-in user’s partition, never a scan.

From → toWhat happensWhat allows it
Worker Lambda → Processed-jobs tableonly when computeEnabled = trueGetItem before sending; a conditional PutItem after sending
  • iam worker role ProcessedJobsTable: dynamodb:GetItem, dynamodb:PutItem on minitickets-dev-processed-jobscreateWorkerRole
See both tables on the map →

DynamoDB → Tables → minitickets-dev-processed-jobs → Explore items; Additional settings → Time to Live.

SymptomCauseWhere to look
Every email job fails; AccessDenied on dynamodb:GetItemThe worker role names another table ARNIAM → worker role → policy
A user gets the same email twiceSent, but the PutItem failed or timed out, so the retry sent againWorker log for that correlation id
My tickets: AccessDenied in the api logThe code tried something other than Queryapi task role
Why DynamoDB and not a table in Postgres?

The Lambda runs outside the VPC and cannot reach Postgres. DynamoDB needs no connection pool for a bursty Lambda, and offers the conditional write and TTL we need.

How would you close the two-workers window?

A lease: write “in progress until now + 60 s” with a conditional write before sending. A second worker backs off. It still cannot close the crash window, and costs one more write per job.

What if the processed-jobs table were lost?

Only possible duplicate emails. That is why point-in-time recovery is off in dev; production would weigh it differently.

Advanced: Details: the tickets table

Partition key userId, sort key ticketId, global secondary index by-event-date (eventDate + eventDateKey). Server-side encryption is on. It stays on when the compute and data switches are off, so issued tickets survive (infra/src/tickets/storage.ts → createTicketStorage).

Answer these without scrolling up. If one feels hard, reread that section.

  1. Why is the job key the booking id and not the message id?
  2. What does attribute_not_exists(jobKey) guarantee?
  3. Why send first and record second?
  4. What does TTL do, and what does it cost?
  5. Which single operation may the api run on the tickets table?

Redraw it from memory: message → GetItem (done?) → yes: skip / no: send with SES → conditional PutItem → TTL deletes after 30 days

Try this: on the map, select the processed-jobs table and open 'Who can touch me' in the Permissions tab

Read this book online: https://book.theversehub.in/