Engineering reference: the order status responder architecture
This is the order status responder with the friendly labels removed: the real resource names, the runtime, the table key schemas, the inbound mail rules, the schedules, and the IAM scope. If you want to build it rather than understand it, start here.
Key takeaways
- Seven Lambda functions, all Python 3.14 on arm64, wired through one SQS queue with a dead-letter queue.
- Four DynamoDB tables, all on-demand: orders mirror, tracking cache (with TTL), threads, and an append-only audit log.
- Inbound is SES for email and two Lambda Function URLs for the SMS and web-chat webhooks — no API Gateway.
- Two EventBridge Scheduler rules: a 15-minute Drive sync and a daily stuck-order sweep.
- One Bedrock model, Claude Haiku 4.5 via Global cross-Region inference, called only by the replier. Single region,
eu-west-2.
The architecture, for engineers
This is the same system as Part 1 with the friendly labels removed and the real resources named. Everything is in one region, eu-west-2 (London), in one account. There is no API Gateway, no NAT Gateway, and nothing always-on; inbound HTTP arrives on Lambda Function URLs, email arrives through SES, and work is buffered on a single SQS queue.
Lambda functions
Seven functions, all Python 3.14 on arm64, all with CloudWatch Logs at 7-day retention. Each does one job and hands off; the SQS queue (osr-jobs, with osr-jobs-dlq as its dead-letter queue after five attempts) decouples intake from the slower carrier and model calls.
osr-intake— the only public surface. Backs the SMS and web-chat Function URLs and the SES inbound rule; normalises the message, runs the order match (number, then sender), and enqueues a job. Unmatched and ambiguous go straight toosr-escalator.osr-tracking— SQS-triggered. Reads the matched order, checksosr-tracking-cache, and on a miss calls the carrier adapter with a key from Secrets Manager; normalises to the five states and writes the cache.osr-replier— makes the single Bedrock call, validates the draft against the facts, sends on the original channel (SES out, or the SMS/chat provider), and writesosr-audit.osr-escalator— builds the handover (message + order + raw tracking + reason), de-duplicates against open escalations inosr-threads, and emails the support inbox via SES.osr-drive-sync— scheduled. Pulls the Drive sheet, writes a snapshot to S3, and upserts rows intoosr-orders.osr-sweep— scheduled. Re-checks in-flight orders against the carrier and escalates those past the no-movement threshold.osr-channel-out— thin sender helper invoked by the replier and escalator; wraps SES and the SMS/chat provider so the calling functions don’t each hold provider logic.
Data stores, schedules, and mail
- DynamoDB (all on-demand).
osr-orders— PKorder_no, GSIs onemailandphonefor the sender fallback.osr-tracking-cache— PKtracking_no, with attlattribute set a few minutes out so cached results expire automatically.osr-threads— PKthread_id, the open-escalation and conversation state used for de-duplication.osr-audit— PKorder_no, SKts, append-only, holding each reply and the exact facts it was built from. - S3.
osr-mail— raw inbound email from the SES receipt rule, plus the latest Drive-sheet snapshot written byosr-drive-sync. Lifecycle expiry on the raw-mail prefix after 30 days. - SES. One inbound receipt rule on the support/order address that writes to
osr-mailand invokesosr-intake; verified domain and DKIM for outbound replies and escalation mail. - EventBridge Scheduler. Two rules —
osr-drive-syncatrate(15 minutes), andosr-sweepat a dailycron(early morning, before business hours). - Secrets Manager. One secret per carrier API and one for the SMS provider; fetched at call time, never in env vars or the sheet.
- Bedrock. Model id
anthropic.claude-haiku-4-5via the Global cross-Region inference profile, invoked only byosr-replier.
IAM scope and region
Each function gets its own execution role scoped to exactly what it touches, no wildcards. osr-intake can read osr-orders and send to osr-jobs; it cannot read the audit table or call Bedrock. osr-tracking can read the cache, write the cache, and read only the carrier secrets — not the SMS-provider secret. osr-replier is the only role with bedrock:InvokeModel, and it’s scoped to the one Haiku profile; it can send via SES and write osr-audit but cannot delete from any table. osr-escalator can write osr-threads and send SES, nothing more. The scheduled functions hold the narrow Drive and carrier permissions they need and no inbound surface at all. Everything runs in eu-west-2; the only cross-Region path is Bedrock’s Global inference profile, which routes the model call for capacity and is not a data store. An AWS Budgets alarm watches the monthly spend and notifies if it drifts above a few dollars — the early signal that a carrier API or a loop is misbehaving.
Design rules that shaped the build
- One job per function. Seven small Lambdas beat one that does everything; the queue decouples the slow calls.
- One public surface. Only
osr-intakeis reachable from outside, on Function URLs and the SES rule. - Least privilege, per role. Only the replier can call Bedrock; only tracking reads carrier secrets.
- State in DynamoDB, blobs in S3. Tables for orders, cache, threads, and audit; S3 for raw mail and the sheet snapshot.
- One region, one model.
eu-west-2throughout; Bedrock Haiku 4.5 via Global inference, called once per reply. - A budget alarm is a smoke detector. The cheapest way to learn something looped is a Budgets alert.