Loading learning content...
Every software system contains values—timeouts, URLs, feature flags, thresholds, connection limits, retry counts, file paths. For each of these values, developers face a fundamental question: Should this be hard-coded, or should it be configurable?
This seemingly simple question conceals profound design trade-offs. Hard-code too much, and your application becomes rigid, requiring code changes for every environmental adjustment. Make everything configurable, and you create complexity nightmares—configuration files with hundreds of options that no one understands, deployment documentation that spans dozens of pages, and production incidents caused by typos in YAML files.
The art of configuration management lies in finding the right balance: making things configurable that need to vary while keeping stable aspects hard-coded for simplicity and reliability.
By the end of this page, you will understand the fundamental principles for deciding what should be configurable versus hard-coded. You'll learn the costs of each approach, see common patterns and anti-patterns, and develop a framework for making these decisions consistently across your systems.
A hard-coded value is any literal value embedded directly in source code. Despite its often-negative connotation, hard-coding is not inherently bad—it's the default approach for values that don't need to change between environments or deployments.
What constitutes a hard-coded value:
12345678910111213141516171819
// Mathematical constants - never changeconst PI = 3.14159265359;const EULER_NUMBER = 2.71828; // Protocol constants - defined by specificationsconst HTTP_OK = 200;const HTTP_NOT_FOUND = 404; // Business logic constants - core domain rulesconst MINIMUM_PASSWORD_LENGTH = 8;const MAX_ITEMS_PER_CART = 100; // Algorithm parameters - determined by analysisconst DEFAULT_HASH_LOAD_FACTOR = 0.75;const BINARY_SEARCH_THRESHOLD = 10; // Internal implementation detailsconst BUFFER_SIZE = 4096;const BATCH_PROCESSING_SIZE = 100;The benefits of hard-coding:
Hard-coded values offer significant advantages that are often underappreciated:
The burden of proof should be on making something configurable, not on hard-coding it. Every configuration option adds complexity. Only introduce configuration when there's a clear, demonstrated need for a value to vary.
Configurable values are those that can be changed without modifying source code—typically through configuration files, environment variables, or configuration services. Configuration becomes necessary when values genuinely need to differ across contexts.
Legitimate reasons to make values configurable:
1234567891011121314151617181920212223242526272829
// Environment-specific - different per deploymentinterface EnvironmentConfig { databaseUrl: string; // postgres://prod-db.example.com:5432/app redisHost: string; // redis-prod.example.com apiBaseUrl: string; // https://api.example.com cdnUrl: string; // https://cdn.example.com} // Secrets - different per environment, require secure handlinginterface SecretsConfig { databasePassword: string; // From secret manager jwtSecret: string; // Rotated periodically externalApiKey: string; // Third-party API credentials} // Operational tuning - may change based on production observabilityinterface TuningConfig { connectionPoolSize: number; // Tuned based on load testing requestTimeoutMs: number; // Adjusted based on latency requirements rateLimitPerMinute: number; // Scaled with capacity cacheExpirationSeconds: number;} // Feature flags - toggled without deploymentinterface FeatureConfig { enableNewCheckoutFlow: boolean; enableDarkMode: boolean; maxItemsPerOrder: number; // Business rule that marketing may change}When deciding what to make configurable, consider your operators—the people who deploy and run your software. What will they need to adjust? What might break in production that they'd want to tune? Configuration should empower operators while not overwhelming them with unnecessary options.
While configurability sounds universally beneficial, it introduces substantial hidden costs that accumulate as systems grow. Understanding these costs is essential for making balanced decisions.
Every configuration option creates:
| Cost Category | Description | Impact |
|---|---|---|
| Cognitive Load | Operators must understand what each option does, its valid values, and its implications | Slower onboarding, increased error probability |
| Documentation | Each option needs documentation explaining purpose, default, valid range, and examples | Documentation maintenance burden, stale docs |
| Testing | Each configuration combination is a potential code path requiring testing | Exponential test matrix, reduced coverage confidence |
| Validation | Invalid configurations must be detected and reported clearly | Additional validation code, error handling complexity |
| Compatibility | Configuration schemas evolve, requiring migration strategies | Version management, backward compatibility burden |
| Debugging | Production issues may be caused by configuration, not code | Harder root cause analysis, configuration drift |
| Security | More configuration surface means more potential misconfiguration vulnerabilities | Security review scope, audit complexity |
The Configuration Explosion Problem:
Consider a system with just 10 boolean configuration options. That's 2^10 = 1,024 potential configurations. How do you test them all? You can't. Now add 5 numeric options with 10 reasonable values each: 1,024 × 10^5 = 10 billion combinations.
This is why every mature organization eventually fights configuration sprawl—the tendency for configuration to grow until it becomes an unmaintainable mess.
1234567891011121314151617181920212223242526272829303132333435363738
// This started simple...interface SimpleConfig { port: number; logLevel: string;} // After 5 years of "just make it configurable"...interface SprawledConfig { port: number; logLevel: string; logFormat: string; logTimestampFormat: string; logTimeZone: string; logRotationEnabled: boolean; logRotationSize: string; logRotationKeepCount: number; logRotationCompress: boolean; logElasticSearchEnabled: boolean; logElasticSearchHost: string; logElasticSearchIndex: string; logElasticSearchBatchSize: number; logElasticSearchFlushInterval: number; // ... 47 more logging options databaseHost: string; databasePort: number; databaseName: string; // ... 89 more database options cacheEnabled: boolean; cacheHost: string; // ... 34 more cache options // Total: 200+ configuration options // 50-page deployment guide // Average onboarding time: 3 weeks // Production incidents from misconfiguration: ~40% of all incidents}Excessive configuration is technical debt. Unlike hard-coded values that can be refactored with IDE support, configuration has users depending on it. Removing configuration options requires deprecation cycles, migration guides, and inevitable breakage. Adding configuration options is easy; removing them is nearly impossible.
Given the trade-offs, how do you decide whether a value should be hard-coded or configurable? Use this decision framework to guide your choices:
The Three Questions:
If the answer to all three questions is 'No', hard-code the value.
The Configurability Spectrum:
Values exist on a spectrum from definitely hard-coded to definitely configurable:
| Category | Examples | Recommendation |
|---|---|---|
| Constants by Definition | HTTP status codes, mathematical constants, protocol values | Always hard-code |
| Domain Invariants | Password minimum length, max cart items (core business rules) | Hard-code; change requires business decision |
| Implementation Details | Buffer sizes, batch sizes, data structure parameters | Hard-code with well-chosen defaults |
| Tuning Parameters | Timeouts, pool sizes, cache durations | Configurable with sensible defaults |
| Environment-Specific | URLs, hosts, ports, paths | Always configurable |
| Secrets | Passwords, API keys, tokens | Always external configuration/secrets manager |
When in doubt, start with a hard-coded constant. If you later discover a need for configuration, extracting a constant to configuration is straightforward. The reverse—removing configuration that users depend on—is painful.
When you decide to hard-code a value, do it properly. 'Hard-coded' doesn't mean magic numbers scattered throughout your code—it means well-organized, documented constants.
Principles for good hard-coded values:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// ✅ GOOD: Named constants with contextnamespace OrderLimits { /** * Maximum items allowed in a single order. * Rationale: Physical warehouse picking capacity is 100 items/order. * Business Decision: Marketing approved 2023-05-14. */ export const MAX_ITEMS_PER_ORDER = 100; /** * Minimum order value for free shipping. * Rationale: Shipping costs are covered above this threshold. * Business Decision: Finance approved 2023-01-10. */ export const FREE_SHIPPING_THRESHOLD_CENTS = 5000; // $50.00 /** * Maximum orders per customer per day. * Rationale: Fraud prevention based on historical fraud patterns. * Security Review: 2023-03-22. */ export const MAX_ORDERS_PER_DAY = 10;} // ✅ GOOD: Type-safe constants with documentationnamespace HttpDefaults { /** Standard timeout for external API calls */ export const DEFAULT_API_TIMEOUT_MS = 30_000; /** Maximum response body size to prevent memory exhaustion */ export const MAX_RESPONSE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB /** Retry count for idempotent operations */ export const RETRY_COUNT = 3; /** Base delay for exponential backoff */ export const RETRY_BASE_DELAY_MS = 1000;} // ❌ BAD: Magic numbers scattered in codefunction processOrder(items) { if (items.length > 100) throw new Error("Too many items"); // Magic! if (getTotalCents(items) >= 5000) waiveShipping(); // Magic! if (getCustomerOrdersToday() >= 10) flagFraud(); // Magic!}MAX_ITEMS_PER_ORDER is better than ONE_HUNDRED.TIMEOUT_MS, SIZE_BYTES, THRESHOLD_CENTS prevent unit confusion.Understanding what not to do is as important as knowing what to do. These anti-patterns recur across organizations:
Anti-Pattern 1: YAGNI Violation (Over-Configuration)
1234567891011121314151617181920
// ❌ BAD: Making everything configurable "just in case"interface OverEngineeredConfig { // These will never change, but someone added them "for flexibility" httpSuccessCode: number; // Always 200 jsonContentType: string; // Always "application/json" maxIntValue: number; // Always 2^31 - 1 trueValue: boolean; // ??? // Implementation details that shouldn't be exposed hashMapLoadFactor: number; // Internal to hash table implementation arrayGrowthFactor: number; // Internal to dynamic array treeRebalanceThreshold: number; // Red-black tree internal constant} // ✅ GOOD: Only configure what needs to varyinterface FocusedConfig { databaseUrl: string; // Varies per environment jwtSecret: string; // Secret, externalized requestTimeoutMs: number; // May need operational tuning}Anti-Pattern 2: Hidden Hard-Coding
The opposite problem—values that should be configurable hidden deep in code:
1234567891011121314151617181920212223242526272829303132333435
// ❌ BAD: Hard-coding environment-specific valuesclass PaymentService { async processPayment(payment: Payment): Promise<Result> { // Hard-coded production URL - works in prod, breaks everywhere else const response = await fetch("https://payments.production.internal/process", { method: "POST", body: JSON.stringify(payment), }); // Hard-coded timeout that can't be tuned await this.waitWithTimeout(response, 30000); // Hard-coded credentials - security nightmare headers: { "Authorization": "Bearer sk_live_abc123xyz789" } }} // ✅ GOOD: Environment-specific values are configurableclass PaymentService { constructor(private config: PaymentConfig) {} async processPayment(payment: Payment): Promise<Result> { const response = await fetch(this.config.paymentApiUrl, { method: "POST", body: JSON.stringify(payment), headers: { "Authorization": `Bearer ${this.config.apiKey}` }, }); await this.waitWithTimeout(response, this.config.timeoutMs); }}Anti-Pattern 3: String-Typed Configuration
12345678910111213141516171819202122
// ❌ BAD: Everything is a stringinterface StringyConfig { port: string; // "8080" - must parse to number maxConnections: string; // "100" - must parse to number enableCache: string; // "true" - must parse to boolean cacheExpiry: string; // "3600" - is this seconds? milliseconds? logLevel: string; // "debug" - what are valid values?} // Leads to error-prone parsing scattered everywhereconst port = parseInt(config.port, 10);if (isNaN(port)) throw new Error("Invalid port");const enableCache = config.enableCache === "true"; // ✅ GOOD: Properly typed configurationinterface TypedConfig { port: number; maxConnections: number; enableCache: boolean; cacheExpirySeconds: number; logLevel: "debug" | "info" | "warn" | "error";}Another subtle anti-pattern is configuration drift—when different environments accumulate different configuration structures over time. Development has 20 options, staging has 35, production has 28, and they're all slightly different. Prevent this with configuration schema enforcement.
Well-organized configuration separates concerns and makes the system understandable. Here are proven organizational patterns:
Pattern 1: Layered Constants
1234567891011121314151617181920212223242526272829303132333435
// constants/// domain/// order-limits.ts <- Business domain constants// pricing-rules.ts// technical/// http-defaults.ts <- Technical defaults// database-defaults.ts// protocol/// http-status.ts <- External protocol constants// mime-types.ts // Domain constants - business decisions// constants/domain/order-limits.tsexport namespace OrderLimits { export const MAX_ITEMS = 100; export const FREE_SHIPPING_THRESHOLD = Money.cents(5000); export const EXPRESS_SHIPPING_CUTOFF_HOUR = 14; // 2 PM local time} // Technical constants - implementation decisions// constants/technical/http-defaults.tsexport namespace HttpDefaults { export const TIMEOUT_MS = 30_000; export const MAX_RETRIES = 3; export const CONNECTION_POOL_SIZE = 10;} // Protocol constants - defined by external specifications// constants/protocol/http-status.tsexport namespace HttpStatus { export const OK = 200; export const CREATED = 201; export const NOT_FOUND = 404; export const INTERNAL_ERROR = 500;}Pattern 2: Configuration by Concern
123456789101112131415161718192021222324252627282930313233343536
// config/// database.config.ts <- Database-specific configuration// redis.config.ts <- Cache-specific configuration// http.config.ts <- HTTP client/server configuration// feature-flags.config.ts <- Feature toggles// index.ts <- Aggregates all configuration // config/database.config.tsexport interface DatabaseConfig { host: string; port: number; database: string; username: string; password: string; // From secrets manager poolSize: number; connectionTimeoutMs: number;} // config/index.tsexport interface AppConfig { database: DatabaseConfig; redis: RedisConfig; http: HttpConfig; features: FeatureFlagsConfig;} // Application receives complete, validated config at startupexport function createApp(config: AppConfig): Application { // Dependency injection with configuration return new Application( new DatabaseConnection(config.database), new RedisClient(config.redis), new HttpClient(config.http), new FeatureService(config.features) );}Generate configuration documentation from your TypeScript interfaces. Tools like TypeDoc can produce reference documentation that stays in sync with your code. This prevents the common problem of configuration documentation drifting from implementation.
The decision between hard-coding and configuration is foundational to system design. Getting it right creates maintainable, operable systems. Getting it wrong creates either rigid systems that can't adapt or configuration nightmares that no one understands.
What's next:
Now that we understand when to make values configurable, we'll explore how to design robust configuration objects and classes. The next page covers configuration design patterns, immutability, builder patterns, and how to create configuration APIs that are easy to use correctly and hard to use incorrectly.
You now understand the fundamental trade-offs between hard-coded and configurable values. You have a decision framework to apply consistently and patterns for organizing constants and configuration. Next, we'll design configuration objects that are robust, validated, and easy to work with.