Skip to content

Part 7 of 7 · Damaged goods claimer series ~7 min read

Engineering reference: the damaged goods claimer architecture

The first six posts are for the person deciding whether to build this. This one is for the person building it. Same system, no analogies: the services by name, the functions, the two tables, the carrier configuration, and the deadline computation.

Key takeaways

  • Single region, single account. Every resource is regional; nothing is global except the IAM roles.
  • 3 Lambda functions, each with its own execution role. No shared role, no wildcards on resources.
  • 2 DynamoDB tables, each keyed so the concurrency story is a condition expression rather than a lock.
  • One Bedrock model, called once, with a JSON schema it must fill or leave null.
  • Nothing always-on: no instance, no container, no provisioned capacity.

The system, by service name

The damaged goods claimer drawn with AWS service namesThree boxes across the top outside the AWS account. The receiving device, sending five photographs. Carrier terms, with their windows and caps. And Whoever files, who also gets reminded. Inside the account, three groups. S3 holding photographs alongside an API providing a capture endpoint. Three Lambda functions named capture, remind and aggregate. And two DynamoDB tables named claims and carriers. A note gives the region as us-east-1, one account, and states that deadlines are stored as dates and packaging holds are released on close.AWS ACCOUNTThe receiving devicefive photographsCarrier termswindows and capsWhoever filesand gets remindedS3 + APIphotographs,capture endpointLambda x3capture, remind, aggregateDynamoDB x2claims, carriersingroundsoutus-east-1. One account. Deadlines stored as dates; packaging holds released on close.
Fig 1. The same shape as Part 1 with the service names filled in. Nothing here is new; it is the same three groups, named.
  • Compute
  • Database
  • Front-end & mobile
  • People

Region and account

  • Region: us-east-1. Chosen because SES inbound receipt rules exist in only a subset of regions and this one has the widest Bedrock model availability. If your data has to stay elsewhere, check both constraints before moving: inbound SES is the binding one.
  • Account: one. This is a small system, and a separate account per environment costs more in wiring than it saves. A dev and a prod stack in the same account, with distinct resource prefixes, is the right size here.
  • Everything is regional. The only global resources are the IAM roles and policies. There is no CloudFront, no global table and no cross-region replication, because nothing here has a latency or durability requirement that would justify them.

Lambda inventory

FunctionTriggerDoesTimeout / memory
dg-captureAPI, from the deviceStores photographs, computes the deadline, generates the note wording and the hold label30s / 1024 MB
dg-remindEventBridge, dailyRuns the reminder ladder; records missed deadlines with their value60s / 512 MB
dg-aggregateEventBridge, weeklyGroups by carrier, route, packaging and supplier; computes rates per 100 consignments120s / 1024 MB

Splitting this into separate functions is not about modularity. It is that only one of them needs Bedrock permissions and only one is reachable from the public internet, and neither of those is true if it is one handler behind a router.

IAM, scoped

RoleAllowedOn
dg-capture-roles3:PutObject, dynamodb:PutItem, dynamodb:GetItemThe photographs prefix; claims; read-only on carriers
dg-remind-roledynamodb:Query, dynamodb:UpdateItem, ses:SendEmailClaims; one verified identity
dg-aggregate-roledynamodb:QueryRead-only across both tables

No role has a Resource: “*” on anything that writes, and every GetSecretValue grant names a single secret arn. That is why there is more than one secret rather than one JSON blob with everything in it.

DynamoDB schemas

Table: claims

PK   claim_ref         S   DG-2026-0184 — printed on the hold label
     carrier           S
     service           S   the cap depends on this, not just the carrier
     delivered_at      S   the clock starts here
     found_at          S   equal to delivered_at, or concealed
     concealed         BOOL
     deadline          S   a date, computed once
     photos            L   5 s3 keys, in the fixed order
     note_annotated    BOOL signed subject to the note?
     goods_value       N
     cap_value         N   recoverable after the carrier’s cap
     decision          S   file | log | filed | recovered | declined | missed
     decision_reason   S   required when the decision is log
     packaging_held    BOOL cleared when the claim closes

`decision_reason` is what makes ’we did not claim for this’ an answer
rather than a gap, a year later.

Table: carriers

PK   carrier#service   S   palletline#economy
     visible_days      N   7
     concealed_days    N   3
     working_days      BOOL true for most
     cap_basis         S   per_kg | per_consignment | declared
     cap_rate_pence    N
     terms_version     S   ’Nov 2025 conditions’
     terms_checked_at  S   2026-04-02

`terms_checked_at` is the field that stops this configuration quietly
going stale and producing late claims that look like process failures.

Inbound and outbound

  • Capture happens on the device in a fixed five-photograph sequence, with a skip that records a reason rather than silently omitting a step.
  • The deadline is computed at capture and stored as a date, using the carrier’s working-day setting and a public holiday calendar.
  • The hold label prints immediately with the claim reference, because the packaging decision happens within the hour.
  • Logged-but-not-filed records use the same table and appear in every aggregation. They are most of the volume.

The model call

  • There is no model in this system. Deadlines are date arithmetic, the cap is a lookup, and the recommendation is four rules.
  • The tempting use is assessing damage severity from the photographs. The carrier will form their own view, and an automated severity score adds nothing to the claim.
  • The wrong use is drafting the claim narrative. A claim is a formal assertion about facts and its wording should be a template a person checked.
  • Classifying damage type from a short picklist at capture is better than inferring it, and it is one tap.
  • The cost page assumes none, which is why messaging and storage are the only variable bands.

Things worth knowing before you build it

  • Store the deadline as a date, not a duration. Arithmetic under pressure over a bank holiday is how valid claims get filed late.
  • Print the hold label at capture. The packaging decision happens within the hour and no email reaches anybody in time.
  • Key the carrier configuration on carrier and service. Caps differ by service, and using the carrier alone produces a recoverable value that is wrong.
  • Include logged-not-filed records in every aggregation. They are most of the volume and all of the frequency signal.
  • Report damage per hundred consignments, never as a count. A count always blames whoever you ship with most.

That is the whole system. Seven posts, one diagram at a time, and nothing in it that needs a server.

All posts