PHP Constants: A Practical Guide for Modern Codebases

A few years ago I debugged a production issue that looked like a database problem. It wasn’t. A single “magic number” had been copy‑pasted into three places, then tweaked in only one of them during a hotfix. The code still ran, the tests still passed, and the bug only appeared for a narrow slice of traffic. The fix was embarrassingly small: name the value once, make it unchangeable, and force the whole codebase to agree.

That’s what constants are for in PHP: a stable identifier for a value you never want to drift at runtime. Constants don’t start with $, they’re immutable after definition, and they’re accessible broadly (with a few important namespace rules). When you use them well, you get fewer accidental changes, clearer intent, and simpler refactors.

I’m going to show you how I define constants in modern PHP, how define() differs from const, how constants behave in namespaces and classes, what built-in constants are actually useful, and the sharp edges that still catch experienced developers.

What a constant really buys you (beyond “a value that doesn’t change”)

When I decide to create a constant, I’m usually buying one (or more) of these properties:

  • Immutability as a safety rail: once defined, a constant can’t be reassigned. That means you can’t “accidentally” change it inside a request handler.
  • Single source of truth: when the rule changes (a limit, a header name, a path prefix), you update it once.
  • Intent: MAXAVATARBYTES tells a future reader more than 2097152 ever will.
  • Better boundaries: constants are a way to expose stable “facts” from a module without exposing state.

A simple analogy I like: variables are sticky notes you can rewrite; constants are labels printed on the box. If the box says “Fragile”, you don’t want someone scribbling “Not fragile” halfway through shipping.

That said, constants are not a replacement for configuration systems. If a value must change per environment, per customer, per request, or during tests, a constant is often the wrong tool. I’ll show the patterns I recommend later.

The real test I use before creating a constant

If I’m unsure, I ask myself a few blunt questions:

  • Would it be a bug if this changed mid-request? If yes, a constant is a good fit.
  • Will I ever need to override this in tests without awkward hacks? If yes, a constant is probably the wrong fit.
  • Is this a protocol detail others must match exactly (header names, cache keys, error codes)? If yes, a constant is usually perfect.
  • Does this value belong to the “shape” of the code, or to the environment it runs in? Code shape → constants; environment → configuration.

That framing keeps me from turning constants into a dumping ground.

Defining constants in PHP: define() vs const (and what I recommend today)

PHP gives you two main ways to define constants:

1) define() (a function)

2) const (a language construct)

Here’s the side-by-side view I keep in my head:

Topic

define()

const

— When it’s evaluated

Runtime

Compile-time (language-level) Where you can use it

Anywhere (including inside if blocks)

Top-level or inside class/interface/trait bodies (not inside conditionals) Namespaces

Can define namespaced constants by including the namespace in the string name

Naturally namespaced via syntax Best fit

Conditional definitions, bootstrapping, polyfills

Most application and library constants Modern preference

Use when you truly need runtime definition

Preferred for structure and readability

A modern define() example (runtime-friendly)

I use define() mainly when I need conditional behavior in a bootstrap file.

<?php

// config/bootstrap.php

if (!defined(‘APP_ENV‘)) {

// Pull from the environment, fall back to a safe default.

define(‘APPENV‘, $ENV[‘APP_ENV‘] ?? ‘production‘);

}

if (!defined(‘APP_DEBUG‘)) {

define(‘APPDEBUG‘, APPENV !== ‘production‘);

}

echo APPENV . PHPEOL;

echo (APPDEBUG ? ‘debug on‘ : ‘debug off‘) . PHPEOL;

Two details matter here:

  • I guard with defined() to avoid warnings and accidental redefinition.
  • I treat these as boot-time facts for the process, not per-request toggles.

A modern const example (my default)

For stable values that are part of the code’s design, const reads cleanly.

<?php

// src/Http/Headers.php

declare(strict_types=1);

namespace App\Http;

final class Headers

{

public const REQUEST_ID = ‘X-Request-Id‘;

public const CLIENT_VERSION = ‘X-Client-Version‘;

}

In day-to-day PHP (framework apps, libraries, packages), this is the style I recommend.

Important: case-insensitive constants are not a modern feature

You’ll still find old snippets that pass a third argument to define() to create a case-insensitive constant. Don’t do that. Case-insensitive constants were deprecated long ago and removed from current PHP versions.

Practical guidance:

  • Treat constant names as case-sensitive.
  • Stick to UPPERCASEWITHUNDERSCORES for global constants and PascalCase class names + UPPERCASE for class constants.

What can you assign to const?

This trips people up because const isn’t “run code and then store the result.” It’s “store a value PHP can determine without running the program.”

In practice, that means const works great for:

  • Scalars (int, float, string, bool, null)
  • Arrays composed of constant values
  • Other constants (self::SOMETHING, PHPVERSIONID, DIR, etc.)

But const doesn’t work for values that require runtime computation (database calls, reading env vars, parsing JSON, calling functions like time()).

If I need runtime computation, I either:

  • Use define() (when it truly must be a constant), or
  • Skip constants and use configuration objects/services.

Scope rules you must internalize: global, functions, and namespaces

People often say “constants are global,” and that’s directionally true, but incomplete. What’s actually going on is:

  • Constants aren’t scoped to functions the way variables are.
  • Constants can be namespaced, and name resolution rules apply.

Constants inside and outside functions

A constant defined at runtime is accessible inside functions without global.

<?php

define(‘MAXLOGINATTEMPTS‘, 5);

function canTryLogin(int $failedAttempts): bool

{

// No ‘global‘ keyword needed.

return $failedAttempts < MAXLOGINATTEMPTS;

}

var_dump(canTryLogin(3)); // true

var_dump(canTryLogin(6)); // false

That “reachable from anywhere” behavior is exactly why constants are great for “facts,” but risky for values that should be injected (database DSNs, credentials, tenant-specific settings).

Namespaced constants and lookup behavior

When you define constants with const inside a namespace, they live in that namespace.

<?php

declare(strict_types=1);

namespace App;

const DEFAULT_TIMEZONE = ‘UTC‘;

function showTimezone(): void

{

// Within the same namespace, unqualified name works.

echo DEFAULTTIMEZONE . PHPEOL;

// You can always be explicit.

echo \App\DEFAULTTIMEZONE . PHPEOL;

}

showTimezone();

My rule: inside shared libraries, I prefer explicit names (\App\DEFAULT_TIMEZONE) at boundaries to make it obvious where a constant comes from.

One subtle point I keep in mind: inside a namespace, an unqualified constant name is resolved by checking the current namespace first, then falling back to the global constant. That fallback is convenient, but it can also hide mistakes if you accidentally rely on a global constant that happens to exist.

If I’m writing reusable code, I either:

  • Use fully-qualified constants (\Vendor\Package\SOME_CONST), or
  • Import them explicitly with use const.

use const is underrated

When code feels noisy with long constant names, I use imports.

<?php

declare(strict_types=1);

namespace App\Http;

use const App\DEFAULT_TIMEZONE;

function debugLine(): string

{

return ‘tz=‘ . DEFAULT_TIMEZONE;

}

That gives me explicitness without repetitive \App\... prefixes.

Defining namespaced constants with define()

define() takes a string, so you can include a namespace in the name.

<?php

declare(strict_types=1);

namespace App;

define(NAMESPACE . ‘\\BUILD_ID‘, ‘2026.02.06‘);

echo \App\BUILDID . PHPEOL;

This is useful in bootstraps and build pipelines where the value is computed at runtime.

One more nuance: if you call define(‘FOO‘, ‘bar‘) inside a namespace, it still defines a constant named FOO (global), not App\FOO. If you want a namespaced constant via define(), you must put the namespace into the string.

Class constants (and why they’re more than “globals with a dot”)

In modern PHP, class constants are one of the cleanest ways to group related fixed values.

Grouping and discoverability

Instead of scattering global constants, group them by concern.

<?php

declare(strict_types=1);

namespace App\Security;

final class PasswordPolicy

{

public const MIN_LENGTH = 12;

public const MAX_LENGTH = 128;

public const HASHALGO = PASSWORDARGON2ID;

}

Visibility: public/protected/private constants

I use constant visibility to keep internal details internal.

<?php

declare(strict_types=1);

namespace App\Crypto;

final class TokenSigner

{

private const HMAC_ALGO = ‘sha256‘;

public static function algorithm(): string

{

return self::HMAC_ALGO;

}

}

echo TokenSigner::algorithm() . PHP_EOL;

This makes refactors safer: you can change HMAC_ALGO without worrying that random code is reading it.

Inheritance and overriding (and when I allow it)

Class constants can be inherited. Whether a child can override a parent constant depends on how you design things.

  • If I’m using constants as part of a stable contract, I usually don’t want overrides.
  • If I’m using constants as “defaults a subclass can specialize,” overrides can be useful.

The key practical detail is how you reference the constant:

  • self::CONST_NAME binds to the class where the code is written.
  • static::CONST_NAME uses late static binding, so subclasses can override behavior by overriding the constant.

Here’s a tiny example that demonstrates the difference:

<?php

declare(strict_types=1);

namespace App\Example;

class Base

{

public const DRIVER = ‘mysql‘;

public static function driverSelf(): string

{

return self::DRIVER;

}

public static function driverStatic(): string

{

return static::DRIVER;

}

}

final class Child extends Base

{

public const DRIVER = ‘pgsql‘;

}

echo Child::driverSelf() . PHP_EOL; // mysql

echo Child::driverStatic() . PHP_EOL; // pgsql

In application code, I keep this pattern rare. If I need polymorphism, I usually prefer methods over overriding constants. But it’s good to recognize when a surprising self:: is the real bug.

Final class constants (when you want to lock them)

In newer PHP versions, you can mark class constants as final to prevent overriding in child classes. If you’re building a base class intended to be extended but you want certain constants to never be overridden (think protocol versions, cryptographic parameters, reserved header names), final is a strong tool.

I still treat final constants as an architectural choice: great when I truly mean “this cannot change,” but easy to misuse if I’m just annoyed by subclass behavior.

Interfaces and traits

  • Interfaces often define constants for protocol-like values, but I avoid stuffing an interface with constants unless they truly define part of the contract.
  • Traits can define constants in recent PHP versions; if you use this, keep it narrow and document the intent. I still prefer a dedicated class for shared constants because it’s easier to navigate.

Prefer enums for finite sets of values

When the “constants” represent a closed set (statuses, roles, payment providers), I recommend enums over a bag of constants.

<?php

declare(strict_types=1);

namespace App\Billing;

enum InvoiceStatus: string

{

case Draft = ‘draft‘;

case Sent = ‘sent‘;

case Paid = ‘paid‘;

case Void = ‘void‘;

}

function canSend(InvoiceStatus $status): bool

{

return $status === InvoiceStatus::Draft;

}

var_dump(canSend(InvoiceStatus::Draft));

Why I push enums here:

  • You get type safety.
  • You can attach behavior (methods) right on the enum.
  • You avoid invalid values drifting through your app.

Constants still win for values that are not a closed set: header names, timeouts, byte limits, and format strings.

Predefined and magic constants you should actually use

PHP ships a lot of predefined constants. Some are trivia; others are extremely practical.

Predefined constants that show up in real systems

A short list I reach for:

  • PHPVERSION and PHPVERSION_ID: useful for diagnostics and support bundles.
  • PHPOS / PHPOS_FAMILY (where available): good for conditional tooling behavior.
  • PHPINTMAX: helps you avoid hard-coding integer limits.
  • DIRECTORY_SEPARATOR: still handy in portable tooling scripts.
  • PHP_EOL: useful for CLI output when you care about platform newlines.

Example: build a diagnostic line without guessing.

<?php

echo ‘PHP ‘ . PHPVERSION . ‘ on ‘ . PHPOS . PHP_EOL;

Magic constants: low-effort, high-value

Magic constants are resolved by PHP and change depending on where they’re used:

  • FILE, DIR: file and directory paths.
  • LINE: line number (mostly for debugging).
  • CLASS, METHOD, FUNCTION: useful in logs.
  • NAMESPACE: helps build namespaced identifiers.

I frequently use DIR for robust path building in small scripts and packages.

<?php

$projectRoot = dirname(DIR);

$configPath = $projectRoot . DIRECTORYSEPARATOR . ‘config‘ . DIRECTORYSEPARATOR . ‘app.php‘;

echo $configPath . PHP_EOL;

If you’re in a framework app, you might already have helpers for paths; still, DIR is a reliable fallback when you’re writing tooling.

A practical PHPVERSIONID guard

When I write small compatibility shims (usually in tooling scripts, not core app logic), PHPVERSIONID is cleaner than parsing strings:

<?php

if (PHPVERSIONID < 80100) {

fwrite(STDERR, "This script requires PHP 8.1+\n");

exit(1);

}

Common mistakes and sharp edges (the stuff that burns time)

Here are the issues I see most often in mature codebases.

1) Trying to define const inside conditionals

This fails because const is not meant for runtime control flow.

Bad pattern:

<?php

if (getenv(‘APP_ENV‘) === ‘development‘) {

const LOG_LEVEL = ‘debug‘; // Not allowed

}

If you truly need conditional definition, use define() with a defined() guard, or better yet, use configuration (arrays/objects) injected where needed.

2) Assuming constants are “safe configuration”

A constant is locked for the lifetime of the process. That’s perfect for:

  • Protocol details (header names)
  • Limits (max sizes)
  • Stable identifiers

It’s a poor fit for:

  • Secrets (you want env vars, secret managers, and scoped access)
  • Values that differ per request (tenant-specific settings)
  • Values that tests need to change easily

If you find yourself wanting to “override” a constant in tests, that’s a strong smell.

3) Redefinition warnings and accidental collisions

Global constants share a global-ish namespace. If two dependencies define MAX_ITEMS, you’re in for a bad day.

Ways I avoid this:

  • Prefer class constants (Pagination::DEFAULTPAGESIZE).
  • Prefer namespaced constants (\Vendor\Package\DEFAULTTIMEOUTMS).
  • If you must use a global constant, prefix it (APPDEFAULTTIMEOUT_MS).

And if you define at runtime, always guard:

<?php

if (!defined(‘APPDEFAULTTIMEOUT_MS‘)) {

define(‘APPDEFAULTTIMEOUT_MS‘, 2500);

}

Also note the failure mode differences:

  • define(‘X‘, 1) when X exists typically emits a warning and returns false.
  • const X = 1; when X already exists is a hard error (and will stop execution).

That’s one reason I keep define() limited to bootstrapping code that’s intentionally defensive.

4) Using constants where a value object or enum would be clearer

If you’re encoding meaning (currency codes, user roles, statuses), constants quickly become “stringly typed.” Enums and small value objects keep those rules explicit.

5) Overusing constant() and dynamic constant names

PHP lets you read constants dynamically:

<?php

define(‘PAYMENTPROVIDERSTRIPE‘, ‘stripe‘);

$provider = constant(‘PAYMENTPROVIDERSTRIPE‘);

echo $provider . PHP_EOL;

This is occasionally handy in metaprogramming, but most application code gets harder to reason about when constant names become strings. If you need dynamic mapping, a keyed array (or enum map) is usually clearer.

6) Forgetting that defined() works with class constants too

When I’m writing robust integration code, I sometimes need to check whether a class constant exists (for example, if I support multiple versions of a dependency). PHP supports that with the ClassName::CONST string format.

<?php

declare(strict_types=1);

if (defined(‘Some\\Vendor\\Thing::FEATURE_FLAG‘)) {

// Safe to reference the constant.

}

I try not to build big systems around this, but it’s useful in small compatibility bridges.

Practical patterns I recommend: constants that age well

This is the part that matters in real projects: how to use constants without turning your code into a museum of fixed values.

Pattern 1: Constants for “public facts” at module boundaries

If your module has a small set of stable facts others must use, expose them as constants.

  • Header names
  • Cache key prefixes
  • Known query param names
<?php

declare(strict_types=1);

namespace App\Cache;

final class CacheKeys

{

public const USERPROFILEPREFIX = ‘user_profile:‘;

public static function userProfile(string $userId): string

{

return self::USERPROFILEPREFIX . $userId;

}

}

This avoids duplicated string formatting all over the app.

A small improvement I often add: put the “how to build a key” next to the prefix constant (as above). That way I don’t end up with multiple slightly-different key formats drifting over time.

Pattern 2: Constants for limits, paired with validation

If a constant represents a limit, I like to pair it with a function that enforces the rule.

<?php

declare(strict_types=1);

namespace App\Uploads;

final class AvatarRules

{

public const MAXBYTES = 2097_152; // 2 MiB

public const ALLOWEDMIMETYPES = [‘image/jpeg‘, ‘image/png‘, ‘image/webp‘];

public static function validate(string $mimeType, int $sizeBytes): void

{

if ($sizeBytes > self::MAX_BYTES) {

throw new \RuntimeException(‘Avatar file is too large.‘);

}

if (!inarray($mimeType, self::ALLOWEDMIME_TYPES, true)) {

throw new \RuntimeException(‘Unsupported avatar format.‘);

}

}

}

The constant carries the “what,” the method carries the “how.” That combo stays readable.

A tweak I like in production code: throw domain-specific exceptions (or return a typed error result) so callers can map failures to user messages cleanly.

Pattern 3: Build-time constants for versioning and observability

In 2026 most teams ship with CI/CD and containers. It’s extremely useful to bake a build identifier into the runtime.

If your pipeline writes a file or exports an environment variable, you can define:

  • APPBUILDID
  • APPGITSHA
  • APPRELEASECHANNEL

I prefer define() here because the value often comes from the environment at runtime.

<?php

// public/index.php or config/bootstrap.php

declare(strict_types=1);

if (!defined(‘APPGITSHA‘)) {

// e.g. injected by the container runtime

define(‘APPGITSHA‘, $ENV[‘APPGIT_SHA‘] ?? ‘unknown‘);

}

if (!defined(‘APPBUILDID‘)) {

define(‘APPBUILDID‘, $ENV[‘APPBUILD_ID‘] ?? date(‘Ymd-His‘));

}

Then I surface it somewhere useful:

  • Response headers (for internal traffic)
  • A /health or /version endpoint
  • Log context (so I can correlate errors with a release)

Pattern 4 (Modern): Prefer configuration objects for environment-specific values

When something is truly configuration, I avoid constants and push it into configuration objects that you can inject.

Traditional approach (hard to test):

Traditional

Modern

Global constants for DSNs and API keys

Config object populated at boot

“Read env var everywhere”

Read env var once, validate once

Hard to override in tests

Easy to override with test configHere’s a minimal pattern I use in plain PHP (no framework assumptions):

<?php

declare(strict_types=1);

namespace App\Config;

final class AppConfig

{

public function construct(

public readonly string $env,

public readonly bool $debug,

public readonly int $httpTimeoutMs,

) {

}

public static function fromEnvironment(): self

{

$env = $ENV[‘APPENV‘] ?? ‘production‘;

$debug = ($env !== ‘production‘);

$timeoutMsRaw = $ENV[‘HTTPTIMEOUT_MS‘] ?? ‘2500‘;

$timeoutMs = (int) $timeoutMsRaw;

if ($timeoutMs < 1 || $timeoutMs > 60_000) {

throw new \RuntimeException(‘Invalid HTTPTIMEOUTMS‘);

}

return new self($env, $debug, $timeoutMs);

}

}

And then I pass AppConfig where it’s needed. The payoff is huge:

  • I validate config once.
  • I can construct different configs for tests.
  • I don’t have to fight immutability when a value genuinely should vary.

I still use constants inside this world, but only for stable facts like default bounds or option names.

Pattern 5: Constants for protocol names and wire formats

Anywhere I’m interfacing with something outside my codebase (HTTP headers, JSON keys, query parameters), constants prevent “almost the same string” bugs.

Example: standardize outgoing headers for internal services.

<?php

declare(strict_types=1);

namespace App\Http;

final class OutgoingHeaders

{

public const REQUEST_ID = ‘X-Request-Id‘;

public const USER_ID = ‘X-User-Id‘;

/ @return array<string,string> */

public static function forUser(string $requestId, string $userId): array

{

return [

self::REQUEST_ID => $requestId,

self::USER_ID => $userId,

];

}

}

Notice what I didn’t do: I didn’t make requestId and userId constants. Those are per-request data and should stay variables.

Pattern 6: Constants as “guardrails” in parsing and validation

Parsing code is where I most often see hard-coded numbers that deserve names: offsets, maximum sizes, default values, allowed algorithm lists.

If I’m parsing tokens, I’ll name the constraints so my future self knows what I was thinking.

<?php

declare(strict_types=1);

namespace App\Auth;

final class BearerToken

{

public const PREFIX = ‘Bearer ‘;

public const MIN_LENGTH = 20;

public const MAX_LENGTH = 4096;

public static function fromHeader(?string $authorization): ?string

{

if ($authorization === null) {

return null;

}

if (!strstartswith($authorization, self::PREFIX)) {

return null;

}

$token = substr($authorization, strlen(self::PREFIX));

$len = strlen($token);

if ($len < self::MINLENGTH || $len > self::MAXLENGTH) {

return null;

}

return $token;

}

}

The constants here document intent: I’m not just slicing strings; I’m enforcing a policy.

Performance considerations (what matters and what usually doesn’t)

Constants are fast. In most real web apps, the difference between reading a constant and reading a variable is not what’s making your endpoints slow.

Still, there are a few performance-related observations I’ve found practical:

1) Constants reduce repeated work when they replace computation

If you’re doing something like:

  • building regex patterns repeatedly
  • repeating expensive string concatenations in hot loops

…then consolidating into a constant (or into a static cached value) can help. But the key point is: the win comes from avoiding repeated work, not from “constants are faster.”

2) Constants help avoid accidental I/O or env lookups in hot paths

A common anti-pattern is reading environment variables all over the place. Even if it’s not expensive, it’s chaotic. I prefer reading env vars once at boot and then using configuration objects.

3) Use constants to improve cache key stability

A performance bug I see a lot: cache key prefixes drift. One service uses user_profile: and another uses userprofile:. Now you’ve doubled your cache surface area and reduced hit rates.

Defining cache key formats in one place (constants + builders) improves both correctness and performance.

A quick “when NOT to use constants” checklist

This is the sanity check I run before adding yet another constant:

  • Secrets: don’t freeze secrets into code constants; use env vars/secret stores.
  • Anything tenant-specific: if different customers need different limits or behavior, constants will fight you.
  • Feature flags: these should change without deploys; constants don’t.
  • Values that tests must override: constants make tests brittle if you need different settings.

If any of those apply, I reach for configuration or dependency injection instead.

Migration playbook: removing magic numbers without making a mess

Turning magic numbers into constants sounds easy until you do it in a real codebase and realize everything is interconnected. Here’s the approach that’s worked best for me.

Step 1: Name only the values that represent a rule

Not every 1 deserves a constant. I usually extract constants for:

  • limits (MAX, MIN)
  • protocol strings (X-* headers, JSON keys)
  • time durations and unit conversions
  • sentinel values that have domain meaning

If it’s just a loop index or a trivial increment, I leave it alone.

Step 2: Put constants where they “belong,” not where they were found

If I find 2097152 inside an upload controller, I don’t necessarily define MAX_BYTES inside that controller. I put it in something like AvatarRules (domain-ish) or UploadRules (module-ish). That reduces the risk of constants turning into random “globals.

Step 3: Add a validating function next to the constant

This prevents the constant from becoming a dead number nobody trusts. MAX_BYTES with no enforcement is just documentation. Documentation is fine, but enforcement is better.

Step 4: Update call sites gradually

If the constant is new, I don’t try to refactor every file in one massive PR unless it’s small. I’ll do:

  • introduce the constant + helper
  • switch the most important call sites
  • follow up with a second pass that replaces remaining duplicates

This keeps the change reviewable.

Extra sharp edges: things I’ve seen in the wild

A few more “surprisingly common” constant-related issues:

Constant names that look like variables

If you name a constant maxBytes instead of MAX_BYTES, it’s not wrong, but it’s easy to miss in review. Conventions matter here because they make scanning safer.

Constants as part of public API

If you publish a library, constants can become part of your public API whether you like it or not. Once external users rely on Vendor\Package\DEFAULTTIMEOUTMS, changing it is a breaking change.

In libraries, I keep public constants intentionally small:

  • Only expose what I’m willing to support long-term.
  • Keep internal constants private or protected.

Putting too much into “Constants.php”

I’ve inherited codebases with a Constants.php file containing 200 unrelated values. That’s usually worse than magic numbers because now you have:

  • no structure
  • unclear ownership
  • lots of accidental coupling

If you want the benefits of constants, grouping matters. I prefer multiple small classes like:

  • Headers
  • CacheKeys
  • PasswordPolicy
  • ErrorCodes

Each one becomes a navigable home for related values.

Closing thought: constants are a design decision

I don’t use constants because “constants are good.” I use them when I want to encode a stable fact about the system and make it hard to accidentally violate.

When I do that well:

  • production bugs get smaller (and rarer)
  • refactors get safer
  • code reads like intent instead of archaeology

And when I do it poorly—when I freeze configuration, or litter globals everywhere—I pay for it later.

If you take one thing from this: treat constants as guardrails for invariants, not as a substitute for configuration.

Scroll to Top