A minimal LocalStack lab to learn an AWS-style async workflow:
- Go HTTP API: Creates and updates requests.
- DynamoDB: Stores request data.
- SQS: Queues status change events.
- Worker: Polls SQS and appends "notification processed" history to DynamoDB.
No UI. Everything is verified via curl and logs.
- User creates a request via API (
PENDING). - Admin updates status via API (
IN_PROGRESS). API pushes an event to SQS. - Worker polls SQS, receives the event, and updates DynamoDB history asynchronously.
sequenceDiagram
participant U as User
participant A as API
participant D as DynamoDB
participant S as SQS
participant W as Worker
U->>A: POST /requests
A->>D: PutItem (Status: PENDING)
A-->>U: Return trackingUrl
Note right of U: Admin Action
U->>A: PATCH /requests/{id}/status
A->>D: UpdateItem (Status: New)
A->>S: SendMessage (Event)
A-->>U: 200 OK
loop Async Processing
W->>S: ReceiveMessage (Long Polling)
S-->>W: Message
W->>D: UpdateItem (Append History)
W->>S: DeleteMessage
end
- LocalStack (AWS Emulator)
- Go
- OpenTofu (
tofu) or Terraform - AWS CLI (
aws) - Make
Create infra/envs/local/terraform.tfvars.
localstack_endpoint = "${YOUR_LOCALSTACK_ENDPOINT}"Create backend/.env.
# App Config
APP_ENV=local
# AWS Config (LocalStack)
AWS_REGION=${YOUR_AWS_REGION}
AWS_ACCESS_KEY_ID=${YOUR_ACCESS_KEY_ID} # e.g. test
AWS_SECRET_ACCESS_KEY=${YOUR_SECRET_ACCESS_KEY} # e.g. test
# Infrastructure Endpoints
DYNAMODB_ENDPOINT=${YOUR_DYNAMODB_ENDPOINT}
SQS_ENDPOINT=${YOUR_SQS_ENDPOINT}
# Application Secrets
# Used for: PATCH /requests/{id}/status
ADMIN_TOKEN=${YOUR_ADMIN_TOKEN}
# Base URL for tracking links (POST /requests response)
APP_PUBLIC_BASE_URL=http://localhost:8080Provision / Destroy Infrastructure:
make infra-apply
make infra-destroyRun Processes: Open two separate terminals for these commands:
make run-backend
make run-workerPerform these steps in a new terminal (Terminal C).
Ensure you have set the same ADMIN_TOKEN in your environment or replace it in the commands below.
make infra-destroy
make infra-applyEnsure make run-backend (Terminal A) and make run-worker (Terminal B) are running.
curl -s http://localhost:8080/health
# Expected: okcurl -s -X POST http://localhost:8080/requests \
-H 'Content-Type: application/json' \
-d '{"title":"test-job"}'Expected JSON:
{
"requestId": "...",
"title": "test-job",
"createdAt": "...",
"trackingUrl": "http://localhost:8080/requests/<id>?t=<token>"
}Action: Copy
requestIdas<REQUEST_ID>andtrackingUrlas<TRACKING_URL>.
curl -s "<TRACKING_URL>"Expected: "status": "PENDING"
Trigger the async workflow.
# Replace <REQUEST_ID> and <ADMIN_TOKEN>
curl -s -X PATCH "http://localhost:8080/requests/<REQUEST_ID>/status" \
-H "Authorization: Bearer ${YOUR_ADMIN_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"status":"IN_PROGRESS"}'Expected JSON:
{
"requestId": "...",
"newStatus": "IN_PROGRESS",
"changedAt": "...",
"eventId": "..."
}Check Worker Logs (Terminal B):
processed eventId=... requestId=... newStatus=IN_PROGRESS
curl -s "<TRACKING_URL>"Expected: "status": "IN_PROGRESS"
Check raw data in DynamoDB to verify the worker appended the history.
# Replace ${YOUR_LOCALSTACK_ENDPOINT}
aws --endpoint-url=${YOUR_LOCALSTACK_ENDPOINT} dynamodb get-item \
--table-name Requests \
--key '{"PK":{"S":"REQ#<REQUEST_ID>"}}'Expected fields:
statusHistory(List)notifiedAt(String)lastEventId(String)
- SQS Long Polling: The worker uses
WaitTimeSeconds: 10. This reduces empty responses and API costs by keeping the connection open until a message arrives. - Visibility Timeout: If the worker crashes while processing a message, the message becomes visible again after the timeout (30s) so another worker can retry it.
make infra-destroy