env.dev

Constants Best Practices

A comprehensive guide to defining, naming, organizing, and using constants effectively across programming languages.

Naming Conventions for Constants

The most universally adopted convention for naming constants is SCREAMING_SNAKE_CASE — all uppercase letters with underscores separating words. This convention is used in C, Python, Java, JavaScript, Go, Rust, and nearly every other mainstream language. When a developer sees MAX_RETRY_COUNT or DEFAULT_TIMEOUT_MS, they immediately understand the value is intended to remain unchanged throughout the program's lifetime.

There are notable exceptions. In JavaScript and TypeScript, const variables that hold primitive values often use camelCase when they serve as module-scoped configuration rather than true compile-time constants. Similarly, Go uses exported identifiers starting with an uppercase letter (MaxRetryCount) rather than screaming case. The key is to be consistent within your project and to follow the conventions of your language's ecosystem.

Prefix constants that belong to a domain or module to avoid naming collisions and improve discoverability. For example, use HTTP_STATUS_OK rather than just OK, and MATH_PI rather than PI if there's any risk of ambiguity. When constants live in a dedicated module or class, the namespace provides the prefix naturally: HttpStatus.OK or math.Pi.

const vs let/var — When to Use const

In languages that offer a const keyword — JavaScript, TypeScript, C++, Rust, Go — the default should be to declare every binding as const unless you know it needs to be reassigned. This principle is sometimes called "const by default" and it produces code that is easier to reason about because you know at a glance which values can change and which cannot.

In JavaScript and TypeScript, const prevents reassignment of the binding but does not make the value immutable. A const object can still have its properties mutated. This is one of the most common sources of confusion for developers coming from languages where const implies deep immutability. To make an object truly immutable, you need additional techniques like Object.freeze() or the as const assertion in TypeScript.

In Rust, all bindings are immutable by default — you opt into mutability with let mut. The const keyword in Rust goes further: it requires a value known at compile time and is inlined everywhere it is used, similar to C's #define but type-safe. In Go, the const keyword is limited to primitive types and compile-time expressions, which makes it less flexible but guarantees that const values are always truly constant.

Frozen and Immutable Objects

When a constant is a compound value — an object, array, or collection — simple const declarations are not enough. You need to ensure that the structure itself cannot be modified after initialization.

In JavaScript, Object.freeze() makes an object shallowly immutable: its direct properties cannot be added, removed, or changed. However, nested objects are not frozen. For deep immutability, you need to recursively freeze every nested object, or use a library like Immer or Immutable.js that provides persistent immutable data structures.

javascript
const CONFIG = Object.freeze({
  maxRetries: 3,
  timeout: 5000,
  endpoints: Object.freeze({
    api: 'https://api.example.com',
    cdn: 'https://cdn.example.com',
  }),
});

// CONFIG.maxRetries = 10; // TypeError in strict mode, silently fails otherwise

In TypeScript, as const narrows the type of a literal expression to its most specific form and marks all properties as readonly. This gives you compile-time immutability without any runtime overhead, which is often the best approach for configuration constants and lookup tables.

typescript
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const;
// type: readonly ['GET', 'POST', 'PUT', 'DELETE']

const STATUS_CODES = {
  ok: 200,
  notFound: 404,
  serverError: 500,
} as const;
// All values are literal types, all properties are readonly

In Java, use Collections.unmodifiableList() or the newer List.of(), Map.of(), and Set.of() factory methods (Java 9+) to create immutable collections. In Python, use tuples instead of lists for fixed sequences, and frozenset instead of set for fixed sets.

Enums vs Named Constants

Enums and named constants both assign meaningful names to fixed values, but they serve different purposes and offer different guarantees. Enums define a closed set of related values — the days of the week, the suits in a deck of cards, the states of a connection. Named constants define individual values that stand alone or group loosely with others.

Use enums when the set of values is finite and known at compile time, when you need exhaustiveness checks (e.g., switch statements that must handle every case), and when the values are semantically a single concept. Use named constants when values are independent, when the set is open-ended, or when you need to assign specific numeric or string values that have external meaning (like HTTP status codes or exit codes).

In TypeScript, the as const pattern with a union type often replaces enums, offering better tree shaking and no runtime overhead:

typescript
// Enum approach
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
  Left = 'LEFT',
  Right = 'RIGHT',
}

// as const approach (preferred in many codebases)
const DIRECTION = {
  Up: 'UP',
  Down: 'DOWN',
  Left: 'LEFT',
  Right: 'RIGHT',
} as const;

type Direction = typeof DIRECTION[keyof typeof DIRECTION];

In Java, enums are the idiomatic choice and can carry methods, fields, and implement interfaces. In Rust, enums are algebraic data types that can hold associated data, making them far more powerful than simple constant enumerations. In Python, the enum module provides proper enum classes with value safety and iteration support.

Single Source of Truth

Every constant in your codebase should be defined in exactly one place. When the same magic number or string appears in multiple files, you create a maintenance burden and a source of subtle bugs. If the value needs to change, you must find and update every occurrence — and missing even one creates an inconsistency that may not surface until production.

Define constants in a dedicated module and import them everywhere they are needed. In a web application, this might be a constants.ts file or a config/ directory. In a library, constants are typically exported from the package's public API so consumers can reference them rather than hardcoding the same values.

The single source of truth principle extends to derived values. If MAX_ITEMS_PER_PAGE is 50 and you need MAX_TOTAL_ITEMS to be 10 pages worth, define it as MAX_ITEMS_PER_PAGE * 10 rather than hardcoding 500. This makes the relationship explicit and ensures that updating one value automatically updates the other.

Grouping Related Constants

Constants that relate to the same concept should be grouped together, whether through a namespace, a class, a module, or an object. Grouping improves discoverability (IDE autocomplete shows all related values), readability (the context is immediately clear), and maintainability (all values that might change together are in one place).

typescript
// Ungrouped — scattered and hard to discover
const ERROR_NOT_FOUND = 404;
const ERROR_SERVER = 500;
const ERROR_UNAUTHORIZED = 401;

// Grouped — clear namespace, discoverable via autocomplete
const HttpStatus = {
  NotFound: 404,
  ServerError: 500,
  Unauthorized: 401,
  Ok: 200,
  Created: 201,
  BadRequest: 400,
} as const;

In Java and C#, static final fields within a class or interface provide natural grouping. In Go, a const block with iota groups related enumerations. In Python, a class with uppercase class attributes or an Enum subclass serves the same purpose. The key is that a developer who finds one constant in the group can immediately see all the others.

Avoiding Magic Numbers and Strings

A magic number is a literal numeric value that appears in code without explanation. A magic string is the same concept applied to string literals. Both are harmful because they obscure the intent of the code: what does if (retries > 3) mean? Is 3 a carefully chosen threshold, an arbitrary default, or a bug?

Replace every magic number and string with a named constant that explains its purpose. The constant name should describe why the value was chosen, not just what it is. MAX_RETRY_ATTEMPTS is better than THREE, and SESSION_TIMEOUT_MS is better than THIRTY_MINUTES.

There are a few universally understood exceptions: 0, 1, -1, "", and true/false are generally acceptable as literals. Array index 0, loop increment 1, and sentinel value -1 do not need named constants. Everything else should be named.

Linters and static analysis tools can help enforce this. ESLint's no-magic-numbers rule, PMD's AvoidLiteralsInIfCondition rule, and similar tools flag unexplained literals and nudge developers toward named constants.

Documentation and Discoverability

Constants deserve documentation just as much as functions and classes. A well-documented constant includes its purpose, its unit of measurement (if applicable), its valid range, and the reason for its specific value. This is especially important for domain-specific constants where the value is not self-evident.

typescript
/**
 * Maximum number of concurrent WebSocket connections per user.
 * Set to 5 based on load testing results — higher values cause
 * memory pressure on the connection broker at current scale.
 */
const MAX_WS_CONNECTIONS_PER_USER = 5;

/**
 * Earth's gravitational acceleration in m/s².
 * Standard value defined by ISO 80000-3.
 */
const GRAVITY_STANDARD = 9.80665;

When constants are well-documented and organized in dedicated modules, they become self-service reference material for your team. New developers can browse the constants file to understand the system's limits, thresholds, and configuration without reading through the implementation code. This is why constants files are often the first file a team member reads when onboarding to a new service.