Skip to content

Part 7 of 7 · Address verifier series ~7 min read

Engineering reference: the address verifier 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 latency budget, and the failure behaviour that matters most.

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 address verifier drawn with AWS service namesThree boxes across the top outside the AWS account. Checkout, which calls a Function URL. The Reference API from a postal data provider. And the Order system, which receives the verification flag. Inside the account, three groups. A Function URL for live checks and EventBridge for the nightly sweep. Three Lambda functions named check, sweep and report. And two DynamoDB tables named cache and addresses. A note gives the region as us-east-1, one account, and states that a lookup failure never blocks a checkout, by design and by test.AWS ACCOUNTCheckouta Function URL callReference APIpostal data providerOrder systemreceives the flagFunction URL + EventBridgelive check,nightly sweepLambda x3check, sweep, reportDynamoDB x2cache, addressesingroundsoutus-east-1. One account. A lookup failure never blocks a checkout, by design and by test.
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
  • Networking
  • Front-end & mobile

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
av-checkFunction URLNormalise, cache lookup, reference call, classify; hard 800ms budget5s / 512 MB
av-sweepEventBridge nightlyRe-checks stored addresses by recency, at a capped rate300s / 512 MB
av-reportEventBridge monthlyDecline patterns, failed delivery correlation, the summary30s / 512 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
av-check-roledynamodb:GetItem/PutItem, secretsmanager:GetSecretValueThe cache table; the reference API credential only
av-sweep-roledynamodb:Query/UpdateItemCache and addresses
av-report-roledynamodb:Query, ses:SendEmailAddresses, read; one verified identity

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: cache

PK   normalised        S   14 chestnut road|ashford|tn24 8ql
     outcome           S   verified | formatting | near_match | not_found
     canonical         M   the reference file’s own form, if matched
     suggestion        M   the near match, if there was exactly one
     looked_up_at      S   2026-08-06T10:14:00Z
     ttl               N   epoch, +12 months

The key is the NORMALISED address, not what was typed. Keying on the raw
string drops the hit rate from about 70% to near zero and pays for the
same lookup repeatedly.

Table: addresses

PK   address_id        S   the order or customer address identifier
     typed             M   exactly as the customer entered it
     stored            M   what was saved, after any accepted suggestion
     outcome           S   verified | formatting | near_match | not_found | unchecked
     suggestion_shown  M   what was offered, or null
     suggestion_taken  BOOL or null
     carrier_verdict   S   what the carrier said at booking, if known
     delivered_ok      N   how many successful deliveries here
     permanent         BOOL true once delivered twice while unverified
     last_checked      S   drives the re-check-on-use rule

`suggestion_shown` with `suggestion_taken` false, repeated across customers,
is what identifies a suggestion rule that is wrong about a real address.

Inbound and outbound

  • The checkout calls a Function URL with an 800 ms client-side timeout. If it does not answer in time the checkout proceeds and the address is recorded as unchecked.
  • That timeout behaviour is a test, not a comment. The single most important property of this system is that it cannot break a checkout, and the way to be sure is to assert it.
  • The reference credential lives in its own secret and is read only by av-check. The sweep uses the same function rather than holding its own copy.
  • The sweep is rate-capped per hour, both to control provider spend and because a backfill hammering a third-party API is a good way to be rate limited during a busy checkout hour.

The model call

  • There is no model in this system. Normalisation is rules, lookup is an API, and the suggestion logic is a comparison.
  • The tempting use is parsing a free-text address into fields, which a model does well — and most reference APIs already offer a parsing endpoint that is cheaper and deterministic.
  • Determinism matters here because the same address must produce the same normalised cache key every time, and a model that phrases one differently on Tuesday breaks the cache.
  • If you do parse with a model, cache the parse by input string so it is called once per distinct input, ever.
  • The cost page assumes none, which is why reference lookups are the only variable band.

Things worth knowing before you build it

  • Cache on the normalised address, never the raw string. It is the difference between a 70% hit rate and a 5% one.
  • Never suggest a different house number. Both numbers usually exist, and a suggestion that sends a parcel to a real stranger is worse than the original typo.
  • Never block, and test that you cannot. A validation timeout that fails a checkout is a self-inflicted outage.
  • Record what was offered and what was chosen. Repeated declines of the same suggestion mean the reference data is wrong about a real address.
  • Report failed deliveries, not addresses verified. One is an outcome and the other is activity.

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

All posts