Part 7 of 7 · Order status responder series ~7 min read

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.

Engineering architecture: inbound edges, SQS, seven Lambdas, four DynamoDB tables, Bedrock, and the scheduled jobs Left column, three inbound edges: SES inbound writing raw email to an S3 bucket osr-mail; an SMS webhook on a Lambda Function URL; and a web-chat webhook on a Lambda Function URL. All three feed osr-intake, which enqueues onto an SQS queue osr-jobs with a dead-letter queue osr-jobs-dlq. Centre column, the processing Lambdas in sequence, all Python 3.14 arm64: osr-intake matches against the osr-orders table; osr-tracking calls external carrier APIs using keys from Secrets Manager and reads and writes the osr-tracking-cache table; osr-replier calls Bedrock Claude Haiku 4.5 and sends the reply out via SES or the channel provider; osr-escalator emails the support inbox. Right column, four DynamoDB tables on-demand: osr-orders keyed by order_no with GSIs on email and phone; osr-tracking-cache keyed by tracking_no with a TTL attribute; osr-threads keyed by thread_id; and osr-audit keyed by order_no and timestamp. Bottom, EventBridge Scheduler with two rules driving osr-drive-sync (pulls the Drive sheet to S3 and into osr-orders every 15 minutes) and osr-sweep (re-checks in-flight orders daily and escalates the stuck ones). External boxes for carrier APIs and the support inbox sit outside a dotted AWS account container that wraps everything else. Bedrock is called only by osr-replier. AWS account · eu-west-2 SES inbound raw email → S3 osr-mail SMS webhook Lambda Function URL Chat webhook Lambda Function URL osr-intake normalise + match osr-jobs SQS + osr-jobs-dlq osr-tracking carrier lookup osr-replier Bedrock + send osr-escalator handover email Secrets Manager Bedrock Haiku 4.5, Global osr-orders PK order_no, GSI email/phone osr-tracking-cache PK tracking_no, TTL osr-threads PK thread_id osr-audit PK order_no, SK ts EventBridge Scheduler 2 rules osr-drive-sync every 15 min osr-sweep daily Carrier APIs external Support inbox external All Lambdas: Python 3.14, arm64 · CloudWatch Logs 7-day retention · AWS Budgets cost alarm
Fig 7. The order status responder drawn for engineers: three inbound edges, an SQS-buffered chain of seven Lambdas, four DynamoDB tables, Bedrock called only by the replier, and two scheduled jobs. One region, one account, no API Gateway.

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 to osr-escalator.
  • osr-tracking — SQS-triggered. Reads the matched order, checks osr-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 writes osr-audit.
  • osr-escalator — builds the handover (message + order + raw tracking + reason), de-duplicates against open escalations in osr-threads, and emails the support inbox via SES.
  • osr-drive-sync — scheduled. Pulls the Drive sheet, writes a snapshot to S3, and upserts rows into osr-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 — PK order_no, GSIs on email and phone for the sender fallback. osr-tracking-cache — PK tracking_no, with a ttl attribute set a few minutes out so cached results expire automatically. osr-threads — PK thread_id, the open-escalation and conversation state used for de-duplication. osr-audit — PK order_no, SK ts, 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 by osr-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-mail and invokes osr-intake; verified domain and DKIM for outbound replies and escalation mail.
  • EventBridge Scheduler. Two rules — osr-drive-sync at rate(15 minutes), and osr-sweep at a daily cron (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-5 via the Global cross-Region inference profile, invoked only by osr-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-intake is 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-2 throughout; 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.
All posts