Configuration Keeping & 12-Factor Discipline
The 3-Line Engineering Rule
Maintain strict separation between code and configuration; compile identical immutable container images and configure them via environment variables.
Building environment-specific binaries (`my-app-dev.jar` vs `my-app-prod.jar`) introduces untested configuration differences and violates modern container principles.
Follow Factor III of the 12-Factor App: store config in the environment. The exact same Docker image must run in local, dev, staging, and production without rebuilding.
Architectural Blueprint & Sequence
Hover over nodes to inspect state transitionsImmutable Image
beingsde/api:v2.4.0 (Identical everywhere)
Dev Environment
ENV: DB_URL=dev.internal:5432
Staging Environment
ENV: DB_URL=stg.internal:5432
Production Cluster
ENV: DB_URL=prod-cluster:5432
Key Engineering Takeaways
- The exact same binary/container image must be deployed to Dev, Staging, and Production.
- All environment differences (DB URLs, third-party keys, thread counts) must come from environment variables.
- Never store credentials in config files committed to code repositories.
- Fail fast on application boot if required environment variables are missing.
The Problem & Production Reality
The 12-Factor App methodology established that an application's configuration is everything that varies between deployment environments (credentials, database URLs, backing services). Code does not vary between environments; only configuration does.
Anti-Pattern: Hardcoded Environment Toggles and Separate Binaries
Checking `if (env.equals("prod"))` in business code or compiling separate builds for each environment.
// ❌ ANTI-PATTERN: Environment logic hardcoded in Java
if (System.getProperty("env").equals("prod")) {
paymentUrl = "https://live.paypal.com";
} else {
paymentUrl = "https://sandbox.paypal.com";
}Production Pattern: Externalized Typed Configuration with Fail-Fast Validation
Map environment variables to strongly-typed configuration classes with strict startup validation.
// ✅ PRODUCTION PATTERN: Strongly-Typed Environment Properties
@ConfigurationProperties(prefix = "payment.gateway")
@Validated
public record PaymentGatewayProperties(
@NotBlank(message = "Payment gateway baseUrl must be specified via PAYMENT_GATEWAY_BASE_URL")
String baseUrl,
@NotBlank(message = "API key must be injected via PAYMENT_GATEWAY_API_KEY")
String apiKey,
@Min(value = 100, message = "Timeout must be at least 100ms")
int timeoutMs
) {}
// application.yml (Default fallback references ENV)
payment:
gateway:
base-url: ${PAYMENT_GATEWAY_BASE_URL}
api-key: ${PAYMENT_GATEWAY_API_KEY}
timeout-ms: ${PAYMENT_GATEWAY_TIMEOUT_MS:2000}Trade-off Analysis & Staff Recommendations
| Aspect | Pros / Strengths | Cons / Hazards | Staff Recommendation |
|---|---|---|---|
| Dotenv (.env) vs System Environment Variables | Dotenv files are convenient for local developer laptops. | Dotenv files can be accidentally committed to Git. | Use `.env` locally (strictly added to `.gitignore`); use real OS environment variables or K8s ConfigMaps in cloud clusters. |
| Default Values | Defaults allow services to boot in test environments with minimal setup. | Dangerous defaults (e.g. `localhost:5432`) can cause silent misconfigurations in staging. | Do not provide defaults for passwords or external URLs; fail fast if missing. |