TL;DR: Separate state per environment in separate backends with separate credentials (workspaces alone are not isolation), share logic through modules rather than directory copies, and promote changes through environments via CI. Enforce each pipeline’s target in its role trust and resource permissions, then test that cross-environment access is denied.
How to approach it
Answer the actual question first (structural isolation), then address the workspaces-versus-directories debate as a secondary detail with its trade-offs. Interviewers listen for whether you know why workspace-per-env fails the safety bar.
A strong answer
The isolation requirement decides the design. "A prod apply can never touch dev" requires access isolation between state objects as well as provider resources, because one mis-set variable or wrong workspace selection becomes an outage. The layout that holds:
modules/ # versioned, shared logic only; no state, no env specifics
envs/
dev/
main.tf # calls modules, small var file
staging/
prod/
Each envs/<name> has its own backend block: a separate state object with enforced access boundaries; separate buckets/accounts are one option, and per-key or per-prefix IAM in a shared bucket is another. Enable supported locking, such as native S3 lockfiles, and critically distinct cloud credentials (per-environment roles or accounts). A staging role should lack production permissions, including indirect paths through role assumption or shared deployment automation. Separate role names alone do not establish that boundary; inspect their effective permissions and trust policies.
Why not CLI workspaces alone? CLI workspaces give each environment its own state file within one backend configuration, which is convenient for identical stacks (per-region previews, per-tenant sandboxes). As environment isolation they fall short: same backend to lose, same provider credentials typically, and switching happens through an easily-forgotten CLI flag (terraform workspace select). Do not rely on selection alone for production isolation. HCP Terraform workspaces are a different facility with their own variables, permissions and execution settings.
Share code, not copy-paste. Modules carry all logic; environment directories carry only inputs (sizes, counts, feature flags). This is what makes the tree maintainable: a change lands once in the module, versions deliberately, and each environment upgrades on its own schedule. Pin module versions per environment (source = "git::...?ref=v1.4.0"), so staging can test v1.5.0 before prod takes it; unversioned module sources make every environment a live experiment.
Use reviewed CI promotion for routine changes. A change merges to main, CI plans-and-applies to dev automatically, then opens the promotion to staging behind review, then prod behind stricter review plus explicit plan inspection. Humans read plans; pipelines hold credentials. Preserve the reviewed plan and apply identity with timestamps. An emergency local apply needs a controlled break-glass identity and its own audit trail. Generate a separate plan for each environment rather than applying a staging plan in production.
Two supporting details worth naming: variable files stay minimal and reviewed (the dangerous deltas should be visible in pull requests, not buried in tfvars nobody reads), and the state backend itself gets locked down (versioning on, access logged, encryption enforced), since state contains secrets and its loss is an incident regardless of layout quality.
What interviewers probe next
"How do environments differ if logic is shared?" Use variables and version pins for intentional variations, and compose modules differently when the architecture requires it. Avoid forcing unrelated architectures into one module with a large set of switches.
"Monorepo versus repo-per-environment?" Use a monorepo when shared review and change coordination fit the ownership model. Separate repositories can support distinct access or release responsibilities. Neither layout replaces cloud authorization or per-environment plans.
"Where do ephemeral preview environments fit?" Same module tree, own workspace-style state under the dev backend, lifetime bound to the pull request. They are the use case workspaces actually serve well.
Common mistakes
Directory trees copied wholesale per environment, diverging silently until someone "fixes" prod with dev's assumptions.
One state file holding all environments because splitting felt premature. The first bad plan teaches the lesson expensively.
Sharing provider credentials across envs while relying on careful typing for safety. Humans typo; IAM boundaries do not.
Sources: Terraform S3 backend permissions.
References
Terraform workspace boundaries and AWS effective policy evaluation.