One SAM template, many AWS accounts
At SOSW, dev and prod are separate AWS accounts. Not separate stacks in one account, not a suffix on a resource name — separate accounts, separate billing, separate blast radius. That decision buys real isolation, but it comes with one non-negotiable obligation: a single SAM template must deploy identically to every account. The moment a template knows which environment it is in, it starts to lie to you. These are the conventions we hold to, and the failure modes each one prevents.
The account is the stage
Because the account boundary already separates environments, the template does not need to. A Lambda function called payments-processor in dev and payments-processor in prod are different resources in every way that matters — different account, different ARN, different IAM world. Encoding the stage a second time inside the resource name adds nothing except a way for the two environments to diverge.
Never bake Stage into resource names
The tempting pattern is Name: !Sub "orders-${Stage}". We forbid it. Once names carry the stage, dev and prod are no longer running the same infrastructure — they are running two infrastructures that happen to share a template. Every consumer of that resource now needs its own stage-aware lookup, every IAM policy needs a stage-interpolated ARN, and every one of those interpolations is a place where dev and prod can quietly drift apart. Cross-account drift is the worst kind: it does not show up in a diff, it shows up in an incident, when the code path that worked in dev resolves a subtly different name in prod.
Exports are the contract — keep them identical everywhere
CloudFormation exports are how our stacks find each other: the queue stack exports the queue ARN, the consumer stack imports it. That export name is a contract, and a contract that changes per environment is not a contract. If the export is orders-queue-arn in every account, a consumer template written once works everywhere. If it is orders-queue-arn-dev in one account and orders-queue-arn-prod in another, every importing template must be told which flavor it is consuming — and now the import chain is stage-aware all the way down. Worse, renaming an export that something imports is a CloudFormation hard stop: the update fails, and untangling it means tearing down the importers first. Stage-suffixed exports guarantee you will hit that wall on the day you can least afford it.
Stage has no default
The Stage parameter exists — some things legitimately differ per environment: log retention, alarm thresholds, a domain name. But it has no Default in the template. You supply it explicitly on the first deploy, and samconfig remembers it after that. The reasoning is blunt: a default is a decision made silently. Default: prod means a hurried deploy into the wrong account configures itself as production. Default: dev means a prod account can end up running with dev-grade retention because someone forgot a flag. Removing the default converts both mistakes into a loud, immediate failure at deploy time — the cheapest possible place to fail.
Naming: stacks carry the type, resources never do
Stack names are lower-case kebab with a type prefix: s3-artifacts, ddb-orders, sqs-ingest-events. The prefix makes a stack list scannable — you can read the account inventory like a table of contents. Resource names inside the stacks never carry the prefix. The bucket in s3-artifacts is artifacts-something, not s3-artifacts-something: the resource type is already visible in the console, in the ARN, in the SDK call, and repeating it just burns name length and invites inconsistency when two people abbreviate differently.
Global names get the AccountId suffix
Most resource names are scoped to the account, so identical names across accounts are fine — that is the whole point. S3 bucket names are the exception: they are globally unique across all of AWS. There the suffix is !Sub "artifacts-${AWS::AccountId}". It is deterministic, needs no parameter, and produces a different name in every account without the template ever knowing about stages. AccountId is the mechanical fact; Stage is an opinion.
Why this matters more with AI agents
We increasingly have AI agents writing and deploying these templates, and the conventions above are precisely what make that safe. An agent operating under these rules cannot accidentally target production, cannot fork the naming scheme per environment, and cannot break an import chain by inventing a stage-flavored export — the invariants are structural, not behavioral. The properties that hold:
- No default Stage means no deploy proceeds on an unstated assumption — the agent must be told the target, every time
- Identical exports mean a template validated against dev is the same contract prod will see
- Stage-free resource names mean there is no per-environment branching for an agent to get subtly wrong
- Deterministic AccountId suffixes remove the one case where names must differ from the space of creative decisions
The pattern generalizes: rules that stop a tired human at 2 a.m. are the same rules that stop a confident model at any hour. Write the template so it cannot know where it is, make every environment difference an explicit input, and one template will serve as many accounts as you ever need.