Part 7 of 7 · Job status notifier series ~7 min read

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.changed events on jsnr-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.

Engineering architecture: the board poll, the event bus, five Lambdas, three DynamoDB tables, S3, SES and SNS, Bedrock, and the schedules A dotted AWS account container labelled eu-west-2 wraps everything except the external job board, the customer, and Bedrock’s Global inference path. Left column: an EventBridge Scheduler with two rules drives jsnr-stage (a few-minute board poll that reads the external job board, compares against the jsnr-jobs table, writes jsnr-stage-history, and puts stage.changed onto the jsnr-bus event bus) and jsnr-sweep (a daily walk of open jobs that emits holding notes and a shop nudge). Centre: the jsnr-bus custom event bus has a rule routing stage.changed to an SQS queue jsnr-notify with dead-letter queue jsnr-notify-dlq; that queue triggers jsnr-composer, which reads jsnr-jobs and the photos bucket, calls Bedrock Claude Haiku 4.5, and hands a message to jsnr-sender; jsnr-sender delivers by SNS for SMS or SES for email and writes jsnr-notifications. A separate jsnr-photo function processes uploads landing in the S3 photos bucket. Right column: three DynamoDB tables on-demand — jsnr-jobs keyed by job_id, jsnr-stage-history keyed by job_id and timestamp, and jsnr-notifications keyed by job_id and timestamp; plus the S3 bucket jsnr-photos. External boxes: the job board and the customer sit outside the container. All Lambdas are Python 3.14 on arm64 with CloudWatch Logs at 7-day retention, and an AWS Budgets alarm watches spend. Bedrock is called only by jsnr-composer; Secrets Manager holds the SMS-sender and board credentials. Job board external · stages Customer external · SMS / email AWS account · eu-west-2 EventBridge Scheduler 2 rules jsnr-stage board poll, detect jsnr-sweep daily, chase stuck jsnr-bus stage.changed + rule jsnr-notify SQS + jsnr-notify-dlq jsnr-composer facts + Bedrock jsnr-sender SNS / SES, quiet hours Bedrock Haiku 4.5, Global jsnr-photo resize + link jsnr-photos (S3) versioned, lifecycle jsnr-jobs PK job_id jsnr-stage-history PK job_id, SK ts jsnr-notifications PK job_id, SK ts Secrets Manager SMS sender, board key All Lambdas: Python 3.14, arm64 · CloudWatch Logs 7-day retention · AWS Budgets cost alarm
Fig 7. The job status notifier drawn for engineers: a scheduled board poll, a custom event bus, an SQS-buffered composer and sender, three DynamoDB tables, an S3 photos bucket, Bedrock called only by the composer, and two scheduled jobs. One region, one account, no API Gateway.

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 in jsnr-jobs, validates known-stage / forward-only / not-seen, appends to jsnr-stage-history, updates the job, and emits one stage.changed event onto jsnr-bus. No customer contact of its own.
  • jsnr-composer — SQS-triggered off jsnr-notify. Gathers facts from jsnr-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 with bedrock:InvokeModel.
  • jsnr-sender — delivers on the customer’s channel (SNS for SMS, SES for email), enforces quiet hours, de-duplicates against jsnr-notifications, and writes the sent record.
  • jsnr-sweep — scheduled daily. Walks open jobs, measures dwell time from jsnr-stage-history against 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 in jsnr-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 — PK job_id; the board mirror, holding customer name, contact, channel, item, current stage, last-recorded stage, and photo links. jsnr-stage-history — PK job_id, SK ts, append-only, one row per recorded move with from-stage and to-stage. jsnr-notifications — PK job_id, SK ts, 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 keyed job_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, carrying stage.changed events, with one rule matching them to the jsnr-notify SQS target. Scheduler holds two rules — the board poll at rate(3 minutes) and the sweep at a daily cron (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-5 via the Global cross-Region inference profile, invoked only by jsnr-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-2 throughout; 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.
All posts