Engineering reference: the job status notifier architecture
This is the job status notifier with the friendly labels removed: the real resource names, the runtime, the table key schemas, the event bus and its rule, the two schedules, and the IAM scope. If you want to build it rather than understand it, start here.
Key takeaways
- Five Lambda functions, all Python 3.14 on arm64, decoupled by a custom EventBridge bus and one SQS queue with a dead-letter queue.
- Three DynamoDB tables, all on-demand: the jobs board mirror, an append-only stage history, and a notifications log.
- Stage changes travel as
stage.changedevents onjsnr-bus; a rule routes them to SQS — no API Gateway anywhere. - Two EventBridge Scheduler rules: a few-minute board poll and a daily quiet-stage sweep.
- One Bedrock model, Claude Haiku 4.5 via Global cross-Region inference, called only by the composer. 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; the board is read by a scheduled poll, stage changes move as events on a custom bus, and work is buffered on a single SQS queue.
Lambda functions
Five functions, all Python 3.14 on arm64, all with CloudWatch Logs at 7-day retention. Each does one job and hands off; the custom bus and the SQS queue (jsnr-notify, with jsnr-notify-dlq as its dead-letter queue after five attempts) decouple watching the board from the slower model and send calls.
jsnr-stage— scheduled poll. Reads the board, compares each job’s stage against the last recorded value injsnr-jobs, validates known-stage / forward-only / not-seen, appends tojsnr-stage-history, updates the job, and emits onestage.changedevent ontojsnr-bus. No customer contact of its own.jsnr-composer— SQS-triggered offjsnr-notify. Gathers facts fromjsnr-jobs, the settings, and the photos bucket; computes the next-stage ETA; makes the single Bedrock call; validates the draft against the facts; and hands a finished message to the sender. The only function withbedrock:InvokeModel.jsnr-sender— delivers on the customer’s channel (SNS for SMS, SES for email), enforces quiet hours, de-duplicates againstjsnr-notifications, and writes the sent record.jsnr-sweep— scheduled daily. Walks open jobs, measures dwell time fromjsnr-stage-historyagainst the per-stage threshold, and for anything stuck (and not already chased) routes a holding note through the composer and sends a digest nudge to the shop.jsnr-photo— triggered by uploads landing injsnr-photos. Confirms the file is an image, resizes it, strips metadata, and records the link on the job. Drops anything invalid.
Data stores, events, and channels
- DynamoDB (all on-demand).
jsnr-jobs— PKjob_id; the board mirror, holding customer name, contact, channel, item, current stage, last-recorded stage, and photo links.jsnr-stage-history— PKjob_id, SKts, append-only, one row per recorded move with from-stage and to-stage.jsnr-notifications— PKjob_id, SKts, the sent log used for dedupe and the audit trail of every message and the facts it was built from. - S3.
jsnr-photos— job photos keyedjob_id/stage, versioning on, with a lifecycle rule that expires photos a set period after a job is collected. - EventBridge. A custom bus,
jsnr-bus, carryingstage.changedevents, with one rule matching them to thejsnr-notifySQS target. Scheduler holds two rules — the board poll atrate(3 minutes)and the sweep at a dailycron(early morning, before opening). - SNS and SES. SNS sends transactional SMS from a registered sender ID / originating number; SES sends email replies and the shop nudge from a verified domain with DKIM.
- Secrets Manager. One secret for the SMS-sender credential and one for the board access the poll uses; fetched at call time, never in env vars or the board itself.
- Bedrock. Model id
anthropic.claude-haiku-4-5via the Global cross-Region inference profile, invoked only byjsnr-composer.
IAM scope and region
Each function gets its own execution role scoped to exactly what it touches, no wildcards. jsnr-stage can read the board secret, read and write jsnr-jobs, write jsnr-stage-history, and put events to jsnr-bus; it cannot call Bedrock or send anything. jsnr-composer is the only role with bedrock:InvokeModel, scoped to the one Haiku profile, and it can read the jobs table and the photos bucket but cannot send directly. jsnr-sender can publish to SNS, send via SES, read the SMS secret, and write jsnr-notifications — and nothing else. jsnr-sweep can read the history and jobs and invoke the composer path, but holds no inbound surface. jsnr-photo can read and write only the photos bucket and update the job’s photo link. 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 cheapest possible early warning that a poll loop or an SMS misconfiguration is running away.
Design rules that shaped the build
- One job per function. Five small Lambdas beat one that does everything; the bus and the queue decouple the slow calls.
- No public surface. Nothing is reachable from outside; the board is polled, and everything else is events and schedules.
- Least privilege, per role. Only the composer can call Bedrock; only the sender can send.
- State in DynamoDB, photos in S3. Tables for jobs, history, and notifications; the bucket for images.
- One region, one model.
eu-west-2throughout; Bedrock Haiku 4.5 via Global inference, called once per update. - A budget alarm is a smoke detector. The cheapest way to learn a poll looped or SMS ran away is a Budgets alert.