Part 7 of 7 · Stock count reconciler series ~7 min read

Engineering reference: the stock count reconciler architecture

This is the reference. The same stock count reconciler, drawn without the business framing — service names, resource identifiers, the region, the Bedrock model id, the Lambda inventory and their Function URLs, IAM scopes, the EventBridge rules and the Scheduler entries, the SQS and dead-letter queue, and the DynamoDB table and key schemas. If you want to build this, or review someone’s build of it, this is the page to read. Everything is prefixed stkr- and lives in one region, eu-west-2.

Key takeaways

  • Seven Lambdas (Python 3.14, arm64), one region (eu-west-2), no API Gateway, no VPC, no always-on compute.
  • stkr-approve carries two Function URLs — approve and reject — signed and tied to a single variance.
  • Three DynamoDB tables on-demand: stkr-inventory, stkr-counts, stkr-adjustments.
  • One Bedrock model — Claude Haiku 4.5 via Global cross-Region inference — reachable only by stkr-cause and stkr-summary.
  • Every IAM role is scoped to its job; only stkr-approve can write a stock quantity.

The architecture, drawn for engineers

Full resource architecture of the stock count reconciler in one region A detailed architecture diagram inside a dotted AWS account container labelled region eu-west-2. Across the top, external sources: stock counters submitting a scanner export or sheet, and the system of record. The sheet or scan lands in an S3 bucket, stkr-counts-bucket, with versioning on. An S3 event goes to EventBridge and onto an SQS queue with a dead-letter queue, which triggers the stkr-capture Lambda. stkr-capture writes clean counted lines to the stkr-counts DynamoDB table and parks unmappable lines. A separate stkr-sync Lambda, fired by EventBridge Scheduler every 15 minutes, mirrors the system of record into the stkr-inventory DynamoDB table using a credential from Secrets Manager. The stkr-variance Lambda reads counted lines from stkr-counts and the snapshot from stkr-inventory, computes each gap, and writes material variances; it invokes stkr-cause for each. stkr-cause calls Bedrock Claude Haiku 4.5 via Global cross-Region inference and returns a likely cause; its role can call Bedrock but cannot write inventory. Material variances are emailed via SES to the approver. The email's two buttons hit two Lambda Function URLs on stkr-approve, which posts an approved adjustment to stkr-inventory with a conditional update and writes an audit row to the stkr-adjustments table. EventBridge Scheduler also fires stkr-sweep daily for the aged-variance sweep and the count reminders, and stkr-summary monthly. CloudWatch Logs retains everything for 7 days, and AWS Budgets watches the spend. A note reads: only stkr-approve may write a stock quantity; the model-facing Lambdas have no inventory write permission. Stock counters scanner export / sheet System of record inventory source AWS account · eu-west-2 S3 stkr-counts-bucket EventBridge + SQS count-submitted, DLQ stkr-capture normalise, park DynamoDB stkr-counts stkr-variance gap = plain Python counted lines DynamoDB stkr-inventory snapshot stkr-sync every 15 min · Secrets Mgr stkr-cause no inventory write Bedrock Haiku 4.5 Global cross-Region inference SES notify material stkr-approve 2 Function URLs: approve/reject conditional post DynamoDB stkr-adjustments (audit) EventBridge Scheduler stkr-sweep (daily + reminders) · stkr-summary (monthly) Only stkr-approve may write a stock quantity — the model-facing Lambdas have no inventory write.
Fig 7. The full system in one region. A count lands in S3, an event drives capture, the variance engine reconciles against the synced snapshot, the cause Lambda calls Bedrock, and only the approve Lambda — behind two Function URLs — ever writes a stock quantity. Everything is logged to stkr-adjustments.

Lambdas (Python 3.14, arm64)

  • stkr-capture — triggered by the SQS queue behind the S3 count-submitted event. Normalises the scan/sheet, ties lines to the open session, writes clean counted lines to stkr-counts, parks unmappable lines. No Function URL.
  • stkr-variance — runs per captured session. Joins counted lines to the stkr-inventory snapshot, computes gap in units/percent/value, applies the materiality tolerance, writes variances, and invokes stkr-cause per material gap. Plain Python; no Bedrock. No Function URL.
  • stkr-cause — builds the evidence bundle and makes one Bedrock call, returning a cause from the fixed list of four plus a rationale. IAM allows bedrock:InvokeModel and reads; no write to stkr-inventory. No Function URL.
  • stkr-approvetwo Lambda Function URLs, one for approve, one for reject, each a signed link scoped to a single variance. Approve does the conditional post to stkr-inventory; both write an audit row to stkr-adjustments. The only Lambda permitted to write a stock quantity.
  • stkr-sync — fired by EventBridge Scheduler every 15 minutes. Reads the system of record using a credential from Secrets Manager and mirrors it into stkr-inventory. No Function URL.
  • stkr-sweep — daily via Scheduler. Re-surfaces aged unactioned variances and issues periodic count reminders for stale locations. No Function URL.
  • stkr-summary — monthly via Scheduler. One larger Bedrock call for the narrative summary, sent by SES. No Function URL.

EventBridge rules and schedules

  • Rule — count-submitted. S3 ObjectCreated under sessions/*/ → SQS (stkr-capture-queue) with a redrive to stkr-capture-dlq after 2 attempts → stkr-capture.
  • Schedule — sync. rate(15 minutes)stkr-sync.
  • Schedule — daily sweep + reminders. cron(0 7 * * ? *)stkr-sweep (respecting quiet hours from the rules doc).
  • Schedule — monthly summary. cron(0 8 1 * ? *)stkr-summary.

DynamoDB tables (on-demand) + key schema

  • stkr-inventory — PK LOC#<location>, SK SKU#<sku>. Attributes: name, system_qty, unit_cost, pack_size, aliases, snapshot_version. Written only by stkr-sync and stkr-approve.
  • stkr-counts — PK SESSION#<id>, SK META for the session header (location, counter, opened_at, status) and SK LINE#<sku> per counted line (counted_qty, state, gap_units, gap_pct, gap_value, cause).
  • stkr-adjustments — PK SKU#<sku>, SK TS#<iso8601>#<variance_id>. Attributes: action (queued/approved/rejected/posted), user, cause, qty_before, qty_after, session_id. Append-only audit trail.

S3, SES, Secrets Manager

  • S3 — stkr-counts-bucket. Versioning on. Layout sessions/<session-id>/{scan,sheet}.csv. Lifecycle: transition raw uploads to Glacier after 90 days.
  • SES. Outbound only — approver notifications and the monthly summary, through one configuration set for bounce/complaint tracking. No inbound rule set (there is no email lane).
  • Secrets Manager. One secret, stkr/sync-credential, read only by stkr-sync.

IAM scopes, Bedrock, region

  • IAM — least privilege per function. stkr-variance: read stkr-counts + stkr-inventory, write variances, invoke stkr-cause. stkr-cause: bedrock:InvokeModel + read only — no inventory write. stkr-approve: conditional UpdateItem on stkr-inventory + write stkr-adjustments. stkr-sync: read the secret, write stkr-inventory. No role has more than its step needs.
  • Bedrock model id. global.anthropic.claude-haiku-4-5-20251001-v1:0, invoked via Global cross-Region inference. Reachable only by stkr-cause and stkr-summary.
  • CloudWatch Logs. 7-day retention on every function’s log group.
  • AWS Budgets. One monthly budget at $20 with an alarm action.
  • Region. Everything in eu-west-2. No API Gateway, no VPC, no NAT Gateway, no always-on compute.

The whole system, in one breath

  • A count lands in S3, an event drives stkr-capture, and clean lines go to stkr-counts.
  • stkr-variance reconciles against the stkr-inventory snapshot in plain Python and flags material gaps.
  • stkr-cause labels each gap with one Bedrock call — and cannot touch stock.
  • stkr-approve, behind two Function URLs, is the only thing that posts an adjustment — after a human taps it.
  • Every action is appended to stkr-adjustments, so the books are always auditable.

That is the stock count reconciler end to end: a count in, a reconciled and explained set of variances out, and not a single stock quantity changed without a person’s sign-off and a row in the audit trail. It reconciles what you have — deciding what to buy is a different system entirely.

All posts