You tick a todo as done · Lambda + DynamoDB

11 steps from your browser to the database and back. Then a background job finishes the work.

InternetAWS Region · us-east-1Your browseron the internetIAMglobal · who can do whatAPI GatewayHTTP API · front doorCloudWatch Logsapp log linesLambda functionruns once per requestDynamoDBTodo.io's tablesWorker Lambdaupdates monthly statsSQS queuetodo-eventsDead-letter queuetodo-events-dlqYou tick a todo as done1Into the front door2Wake up the function3Who are you?4Get AWS credentials5Call DynamoDB6Is this allowed?7DynamoDB answers8Drop a job in the queue9Back to you10Leave a note11The worker picks it up12Save the result13Job done14If the worker keeps failing15

Text version of this diagram.

What sits where:

  • AWS Region · us-east-1: A Region is a part of the world where AWS has data centers. Most things you build live in one Region. us-east-1 is in northern Virginia. contains:
    • API Gateway (HTTP API · front door): The front door for the app's web requests: it receives each one and hands it to your code. An HTTP API is its simple kind for ordinary requests.
    • CloudWatch Logs (app log lines): Collects the lines your app writes about what it's doing (its logs), so you can search them later.
    • Lambda function (runs once per request): Code that AWS runs only when a request arrives. No server to look after, and you pay per request.
    • DynamoDB (Todo.io's tables): AWS's serverless NoSQL database: you store and fetch items by key, and it scales by itself with no server to manage.
    • Worker Lambda (updates monthly stats): A second Lambda function that does slow work in the background, taking jobs off the queue.
    • SQS queue (todo-events): Simple Queue Service: a waiting line for messages. One part of the app drops a job in; another picks it up later.
    • Dead-letter queue (todo-events-dlq): A dead-letter queue: where a message goes after failing several times, so someone can look at it instead of it being retried forever.
  • Your browser (on the internet)
  • IAM (global · who can do what): Identity and Access Management: decides who (and which app) may do what in AWS. It's global, the same in every Region.

Steps, in order:

  1. You tick a todo as done. Path: Your browser. Your browser sends PATCH /todos/42 over HTTPS.
  2. Into the front door. Path: Your browser → API Gateway. The request arrives at API Gateway, a managed HTTPS front door. There's no server or firewall for you to look after.
  3. Wake up the function. Path: API Gateway → Lambda function. API Gateway invokes your Lambda function and hands it the request as an event. Checks: iam: Lambda resource policy: API Gateway may invoke todo-api → allow.
  4. Who are you?. Path: Lambda function. The app checks your signed session cookie and finds your user id, u-123. This is the app's own login, not AWS.
  5. Get AWS credentials. Path: Lambda function. The app gets temporary credentials for its IAM role (the execution role) which Lambda already put in its environment variables. No keys are stored in the code.
  6. Call DynamoDB. Path: Lambda function → DynamoDB. The function calls UpdateItem on the DynamoDB API over HTTPS, signed with those credentials.
  7. Is this allowed?. Path: DynamoDB → IAM. DynamoDB checks with IAM: may todo-app-lambda-role do dynamodb:UpdateItem here? The policy on the role allows it on the table's ARN. Checks: iam: todo-app-lambda-role: dynamodb:UpdateItem on table/Todos → allow.
  8. DynamoDB answers. Path: DynamoDB → Lambda function. DynamoDB confirms the item changed.
  9. Drop a job in the queue. Path: Lambda function → SQS queue. The app sends a message to an SQS queue saying a todo was completed, instead of doing the slow work now. Checks: iam: todo-app-lambda-role: sqs:SendMessage on todo-events → allow.
  10. Back to you. Path: Lambda function → API Gateway → Your browser. You get 200 OK, and the todo shows a tick.
  11. Leave a note. Path: Lambda function → CloudWatch Logs. Lambda sends what the function printed to CloudWatch Logs, so Todo.io's engineers can see what happened.
  12. The worker picks it up. Path: SQS queue → Worker Lambda. Lambda keeps polling the queue for you and runs the worker function with the new message. Checks: iam: todo-worker-role: sqs:ReceiveMessage, sqs:DeleteMessage → allow.
  13. Save the result. Path: Worker Lambda → DynamoDB. The worker adds 1 to your monthly stats in DynamoDB, using its own role. Checks: iam: todo-worker-role: dynamodb:UpdateItem on table/Stats → allow.
  14. Job done. Path: Worker Lambda → SQS queue. The worker returns without an error, so the Lambda service deletes the message from the queue. Your code never deletes it.
  15. If the worker keeps failing. Path: SQS queue → Dead-letter queue. If the worker fails on the same message 3 times, SQS moves it to a dead-letter queue. It isn't lost, and it stops being retried forever.
Step 1 of 15
Step 1 of 15
  1. Your browser sends PATCH /todos/42 over HTTPS.

    Show me the real thing

    The HTTP request

    PATCH /todos/42 HTTP/1.1
    Host: abc123.execute-api.us-east-1.amazonaws.com
    Cookie: session=eyJ1c2VySWQiOiJ1LTEyMyJ9.Zm9v
    Content-Type: application/json
    
    {"done":true}
  2. The request arrives at API Gateway, a managed HTTPS front door. There's no server or firewall for you to look after.

  3. API Gateway invokes your Lambda function and hands it the request as an event.

    • 🔑IAM Lambda resource policy: API Gateway may invoke todo-apiAllowed
    Show me the real thing

    Resource-based policy on the function: who may call it

    {
      "Effect": "Allow",
      "Principal": {
        "Service": "apigateway.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:us-east-1:111122223333:function:todo-api",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:execute-api:us-east-1:111122223333:abc123/*"
        }
      }
    }

    Exam tipThis is a resource-based policy. It lives on the function and says who may call it, the opposite of a policy attached to a role.

  4. The app checks your signed session cookie and finds your user id, u-123. This is the app's own login, not AWS.

  5. The app gets temporary credentials for its IAM role (the execution role) which Lambda already put in its environment variables. No keys are stored in the code.

    Show me the real thing

    Trust policy on todo-app-lambda-role: who may use this role

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Principal": {
            "Service": "lambda.amazonaws.com"
          },
          "Action": "sts:AssumeRole"
        }
      ]
    }

    Exam tipEvery Lambda function has exactly one execution role. Its permissions are everything your code can do in AWS.

  6. The function calls UpdateItem on the DynamoDB API over HTTPS, signed with those credentials.

    Show me the real thing

    The UpdateItem request body

    {
      "TableName": "Todos",
      "Key": {
        "userId": {
          "S": "u-123"
        },
        "todoId": {
          "S": "42"
        }
      },
      "UpdateExpression": "SET done = :d",
      "ExpressionAttributeValues": {
        ":d": {
          "BOOL": true
        }
      }
    }

    Exam tipThis function isn't attached to a VPC, so it reaches DynamoDB directly. No VPC, no security groups.

  7. DynamoDB checks with IAM: may todo-app-lambda-role do dynamodb:UpdateItem here? The policy on the role allows it on the table's ARN.

    • 🔑IAM todo-app-lambda-role: dynamodb:UpdateItem on table/TodosAllowed
    Show me the real thing

    Permissions policy on todo-app-lambda-role

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "dynamodb:GetItem",
            "dynamodb:Query",
            "dynamodb:PutItem",
            "dynamodb:UpdateItem",
            "dynamodb:DeleteItem"
          ],
          "Resource": [
            "arn:aws:dynamodb:us-east-1:111122223333:table/Users",
            "arn:aws:dynamodb:us-east-1:111122223333:table/Todos"
          ]
        }
      ]
    }

    Exam tipIAM starts from deny. A request is only allowed if some policy explicitly allows it and nothing explicitly denies it.

  8. DynamoDB confirms the item changed.

  9. The app sends a message to an SQS queue saying a todo was completed, instead of doing the slow work now.

    • 🔑IAM todo-app-lambda-role: sqs:SendMessage on todo-eventsAllowed
    Show me the real thing

    SendMessage request

    {
      "QueueUrl": "https://sqs.us-east-1.amazonaws.com/111122223333/todo-events",
      "MessageBody": "{\"type\":\"todo.completed\",\"userId\":\"u-123\",\"todoId\":\"42\"}"
    }

    Exam tipA queue decouples the app from the worker. If the worker is slow or broken, messages simply wait (up to 14 days).

  10. You get 200 OK, and the todo shows a tick.

    Show me the real thing

    The HTTP response

    HTTP/1.1 200 OK
  11. Lambda sends what the function printed to CloudWatch Logs, so Todo.io's engineers can see what happened.

    Show me the real thing

    The log line

    2026-10-11T18:42:07Z INFO PATCH /todos/42 user=u-123 status=200 14ms

    Exam tipLambda sends whatever your function prints to CloudWatch Logs automatically, as long as the execution role allows logs:PutLogEvents.

  12. Lambda keeps polling the queue for you and runs the worker function with the new message.

    • 🔑IAM todo-worker-role: sqs:ReceiveMessage, sqs:DeleteMessageAllowed
    Show me the real thing

    The event the worker receives (trimmed)

    {
      "Records": [
        {
          "messageId": "19dd0b57-b21e-4ac1",
          "eventSource": "aws:sqs",
          "eventSourceARN": "arn:aws:sqs:us-east-1:111122223333:todo-events",
          "body": "{\"type\":\"todo.completed\",\"userId\":\"u-123\",\"todoId\":\"42\"}"
        }
      ]
    }

    Exam tipSQS never pushes. Lambda polls it for you (an "event source mapping"). Set the queue's visibility timeout to at least 6× the function's timeout.

  13. The worker adds 1 to your monthly stats in DynamoDB, using its own role.

    • 🔑IAM todo-worker-role: dynamodb:UpdateItem on table/StatsAllowed
    Show me the real thing

    The UpdateItem request body

    {
      "TableName": "Stats",
      "Key": {
        "userId": {
          "S": "u-123"
        },
        "month": {
          "S": "2026-10"
        }
      },
      "UpdateExpression": "ADD completed :one",
      "ExpressionAttributeValues": {
        ":one": {
          "N": "1"
        }
      }
    }

    Exam tipStandard queues deliver at least once, so the same message can arrive twice and count your todo twice. Real workers are idempotent: they record each message id and skip repeats.

  14. The worker returns without an error, so the Lambda service deletes the message from the queue. Your code never deletes it.

    Exam tipIf the worker throws, the message isn't deleted. It becomes visible again after the visibility timeout and is retried.

  15. If the worker fails on the same message 3 times, SQS moves it to a dead-letter queue. It isn't lost, and it stops being retried forever.

    Show me the real thing

    Redrive policy on todo-events

    {
      "deadLetterTargetArn": "arn:aws:sqs:us-east-1:111122223333:todo-events-dlq",
      "maxReceiveCount": 3
    }

    Exam tipA dead-letter queue must be the same type as its source queue: a FIFO queue needs a FIFO DLQ.

What to notice

  • There is no server at all. API Gateway takes the request and Lambda runs your code only when one arrives.
  • IAM approved every database call, using a role. There's no database password anywhere.
  • DynamoDB isn't in your VPC. It's a public AWS service called over HTTPS.
  • You got your answer before the job ran. The queue lets slow work happen later without making you wait.
  • If the worker breaks, messages wait in the queue, and after 3 failures they land in the DLQ. Nothing is lost.
  • You, the customer, were never an IAM user. IAM controls the app, not its customers.