Configuration, Secrets, and Twelve-Factor Boundaries

LESSON

Backend Development Foundations

012 25 min beginner

Configuration, Secrets, and Twelve-Factor Boundaries

By the end of this lesson, you will be able to...

  • Separate code, deploy-time configuration, and secrets in a backend service.

  • Trace startup configuration loading from raw strings to a safe readiness decision.

  • Recognize configuration drift, unsafe secret logging, and rotation risks before a service handles traffic.

Idea in one sentence: Configuration is deploy-time input for one environment; secrets are sensitive configuration that need stricter storage, logging, and rotation boundaries.

Core Insight

The previous lesson asked: which test boundary catches this failure?

Now the orders API passes its tests. The artifact deploys. Production starts.

Then checkout begins failing.

The worker logs look normal at first:

worker started
queue connected
payment client initialized

Payment authorizations fail for real customers. After an hour, the team finds the cause:

APP_ENV=production
PAYMENT_API_BASE_URL=https://staging-payments.example
PAYMENT_API_KEY=staging key

The code was correct. The tests were correct for the code they exercised. The deploy-time configuration was wrong.

This is a different backend failure from a bad SQL query, a missing authorization check, or a provider contract mismatch. The same code changed behavior because values outside the source code pointed it at the wrong environment.

The naive idea is:

Configuration is just a bag of strings the app reads when it needs them.

That is too loose.

A better model is:

Configuration is a startup boundary.
The service receives deploy-time instructions.
It parses them.
It validates them.
It refuses unsafe combinations.
It reports safe evidence.
Only then should it become ready.

Secrets fit inside that model, but they need tighter handling. A payment key, database password, or session signing secret is not just another string. Possession may grant access.

The Small Situation

Use one service: orders-worker.

It processes checkout jobs and calls a payment provider.

It needs values like:

APP_ENV=production
DATABASE_URL=postgres://...
PAYMENT_API_BASE_URL=https://api.payments.example
PAYMENT_API_KEY=...
SESSION_COOKIE_SECRET=...
ORDER_QUEUE_NAME=orders-production
LOG_LEVEL=info
FEATURE_NEW_CHECKOUT=false
REQUEST_TIMEOUT_MS=3000

These values are not all the same kind of thing.

Plain meaning:

Configuration tells the same release how to run in one place.

In this scenario:

PAYMENT_API_BASE_URL tells the worker which payment provider endpoint to call in production.

Technical name:

This is deploy-time configuration. The Twelve-Factor App guideline says configuration that varies between deploys should be separated from code. Environment variables are one common interface, but the deeper idea is the boundary: code and deploy-specific values should not be tangled together.

Plain meaning:

A secret is a configuration value that must not be casually exposed.

In this scenario:

PAYMENT_API_KEY and SESSION_COOKIE_SECRET are secrets because they can authorize sensitive actions or protect authentication evidence.

Technical name:

These values need secret management: controlled storage, injection, access limits, redaction, and rotation.

A Startup Configuration Trace

Trace the worker from process launch to readiness.

Input:
  raw environment variables
  secret references from deployment platform

Step 1: process starts
  no checkout jobs should run yet

Step 2: read raw values
  APP_ENV="production"
  REQUEST_TIMEOUT_MS="3000"
  FEATURE_NEW_CHECKOUT="false"
  PAYMENT_API_KEY is present from secret manager

Intermediate state:
  values are still strings or secret handles

Step 3: parse typed settings
  app_env = production
  request_timeout_ms = 3000
  new_checkout_enabled = false
  payment_api_key = present secret

Step 4: validate required values
  DATABASE_URL exists
  PAYMENT_API_BASE_URL exists
  PAYMENT_API_KEY exists
  SESSION_COOKIE_SECRET exists

Step 5: validate meaning
  timeout is within allowed range
  log level is allowed
  production does not point at staging payment URL
  production queue name matches production environment

Step 6: initialize clients
  database client created
  payment client created
  queue client created

Step 7: emit safe startup evidence
  app_env=production
  payment_base=api.payments.example
  payment_key=present version=payments-prod-v4
  order_queue=orders-production
  timeout_ms=3000

Output:
  service becomes ready for work

Now compare the broken path:

Input:
  APP_ENV=production
  PAYMENT_API_BASE_URL=https://staging-payments.example
  PAYMENT_API_KEY=staging key

Naive startup:
  every variable exists
  worker becomes ready
  checkout jobs call staging provider
  production payments fail

The missing check was not "is the value present?"

The missing check was:

Are these values coherent for this environment?

That is why startup validation should check presence, type, range, and unsafe combinations.

Check: APP_ENV=production and PAYMENT_API_BASE_URL=https://staging-payments.example are both present and parseable. Should startup validation pass?

Think first, then reveal.

Answer: No. The values exist, but they are incoherent. A production worker should not silently call a staging payment provider unless there is an explicit, controlled exception.

Secrets Need Different Handling

All secrets are configuration.

Not all configuration is secret.

LOG_LEVEL=info is configuration, but it is usually not secret. PAYMENT_API_KEY is configuration and secret. Treating both the same creates risk.

Secrets need stricter handling because possession often grants power:

Good secret handling includes:

Safe logs should describe secret state without revealing secret material.

Prefer:

payment_api_key=present version=payments-prod-v4
session_cookie_secret=present version=session-2026-07

Avoid:

PAYMENT_API_KEY=sk_live_full_secret_value_here
SESSION_COOKIE_SECRET=full_secret_here

Even partial secrets can be risky. Printing the first eight characters may feel harmless, but providers sometimes encode meaning in prefixes, and partial leaks can combine with other evidence. Use secret version IDs, names, or metadata instead.

Check: Should startup logs print the first eight characters of a production API key to help debugging?

Think first, then reveal.

Answer: Usually no. Prefer safe presence checks and secret version metadata. Logs should help operators debug without leaking key material.

Rotation Is a Workflow

Changing a secret is not always one edit.

Suppose SESSION_COOKIE_SECRET signs user sessions.

If you replace it instantly, every existing session may fail verification. That may be acceptable during an incident. It is usually too disruptive for routine rotation.

A rotation-friendly design might use one key for signing and multiple keys for verification:

before:
  sign with: key_2026_06
  verify with: key_2026_06

during rotation:
  sign with: key_2026_07
  verify with: key_2026_07, key_2026_06

after old sessions expire:
  sign with: key_2026_07
  verify with: key_2026_07

Payment keys may need a different workflow:

1. create new provider key with the right permissions
2. deploy support for reading the new secret version
3. switch traffic to the new key
4. verify payment success and error rates
5. revoke the old key

The constant idea is:

Rotation changes live behavior.
Plan overlap.
Verify success.
Remove old access.

Rotation is also a testing and deployment concern. If the app only supports one signing key, routine rotation becomes a user-facing logout event. If workers cache secrets forever, rotation may not take effect until every worker restarts.

Drift, Flags, and Safe Evidence

Configuration drift means environments differ in ways the team cannot quickly explain.

Some differences are intended:

local uses local database
staging uses staging payment provider
production uses live payment provider

The dangerous differences are untracked or unvalidated:

production has an old timeout
staging uses a different feature flag
one worker has a stale secret version
debug logging is accidentally enabled in production

Feature flags are configuration too. A flag such as FEATURE_NEW_CHECKOUT=true can help gradual rollout. It can also make staging and production exercise different code paths. For critical paths, know who owns the flag, when it expires, and what evidence shows which value was active.

Good operational evidence is safe and specific:

app_env=production
release_sha=abc123
config_version=orders-prod-2026-07-01
new_checkout_enabled=false
payment_provider=live
payment_api_key_version=payments-prod-v4

This evidence does not replace secret protection. It helps operators answer:

Do not log every configuration value on every request. That creates noise and risk. Prefer startup summaries, deployment records, readiness state, and safe metrics dimensions with low cardinality.

A Small Decision Table

When reviewing a new setting, ask what kind of value it is before deciding where it belongs.

Value                         Kind                         Handling
APP_ENV                       config                       validate allowed value
REQUEST_TIMEOUT_MS            config                       parse integer and range
FEATURE_NEW_CHECKOUT          config                       record owner and expiry
PAYMENT_API_BASE_URL          config                       validate environment match
PAYMENT_API_KEY               secret config                inject securely, redact logs
SESSION_COOKIE_SECRET         secret config                support rotation overlap
DATABASE_URL                  sensitive config             restrict access, avoid logs

This table helps because "configuration" is not one storage bucket. A timeout, a feature flag, and a signing secret all arrive through the deploy boundary, but they do not deserve the same treatment.

A timeout should be easy to see in safe startup evidence because it affects behavior and helps debugging. A feature flag should have ownership because it changes which code path runs. A payment key should be present and versioned, but not visible. A session signing secret should have a rotation plan because old sessions may still need verification.

The design review question is:

If this value is wrong, when do we want to find out?

For required values, the answer is usually startup. Missing DATABASE_URL or malformed REQUEST_TIMEOUT_MS should stop readiness before the service handles work.

For dangerous combinations, the answer is also startup. Production pointed at staging payments is not a runtime surprise we should let customers discover.

For values that intentionally vary, the answer is safe evidence. Operators should be able to see that FEATURE_NEW_CHECKOUT=false was active for release abc123 without exposing secrets or dumping every environment variable.

This connects back to testing. A unit test can prove the parser rejects "ten_seconds". An integration or deployment smoke test can prove the service refuses readiness with incoherent production settings. But the operational habit is bigger than one test: make configuration reviewable as a boundary.

Trade-offs and Limits

Separating code from configuration makes deployment flexible. The same artifact can run locally, in staging, and in production.

It also creates a new failure mode: the artifact may be correct while the environment instructions are wrong.

The main trade-off is flexible deployment versus configuration drift.

Moving values out of code means one release can run in many environments. That is useful. It also means two environments can quietly diverge until the wrong value causes a production failure.

Startup validation improves safety because the service fails before handling traffic. It costs discipline. Teams must maintain a schema for settings, reject unsafe combinations, and keep environment rules current.

Secret management reduces leak risk. It costs operational work: access control, rotation planning, audit trails, and platform integration.

Safe config evidence improves debugging. It costs judgment because too much detail leaks information or creates noisy logs.

This lesson does not prove dependencies are healthy. A valid DATABASE_URL can still point to a down database. Configuration validation answers:

Did the service receive coherent instructions?

Readiness and health checks answer:

Can the service use what it needs right now?

The signal that this boundary is weak is an incident where the code and tests are fine, but a deploy-time value makes the service behave like the wrong environment.

Practice

Review this deployment configuration:

APP_ENV=production
DATABASE_URL=postgres://orders-prod
PAYMENT_API_BASE_URL=https://staging-payments.example
PAYMENT_API_KEY=present
SESSION_COOKIE_SECRET=present
LOG_LEVEL=debug
REQUEST_TIMEOUT_MS=ten_seconds
FEATURE_NEW_CHECKOUT=true

A good answer should mention:

Model answer:

This service should not become ready. The values are present, but the production environment points at the staging payment provider, the timeout is not parseable as an integer, and debug logging in production is risky unless explicitly approved. The startup summary can say payment_key=present and name a secret version, but it should not print the key. For session secret rotation, the service should support signing with the new key while verifying old sessions until they expire.

Connections

Lesson 011 asked what a test boundary can prove. This lesson shows a failure that tests may miss if the wrong values are injected at deploy time.

The next lesson uses the same boundary thinking for containers: what belongs in the deployable image, and what must stay outside as runtime configuration or external dependency state.

Resources

Key Takeaways

PREVIOUS Testing Backends: Unit, Integration, and Contract Tests NEXT Containers, Local Development, and Deployment Artifacts