Magic numbers anti-pattern
Learn why unnamed numeric constants scattered through code create maintenance problems, and how named constants, enums, and configuration make their meaning and ownership explicit.
A magic number is a literal value whose meaning is not clear from the code around it. The concern is not that literals are forbidden; it is that a domain rule, unit, or shared value is hidden inside an expression where readers and maintainers cannot name or verify it easily.
TL;DR
- Magic numbers are literal values embedded in code with no explanation of what they represent or why that specific value was chosen.
if (retries > 3)does not reveal intent by itself. Is 3 the max retries, a timeout multiplier, or a coincidence? The literal offers little help without surrounding context.- Duplicated domain values are especially risky: the same threshold copied across several places can be updated in one path and left stale in another.
- Named constants (
static final), enums for domain sets, and externalized configuration are the three-tier fix. - The "zero, one, infinity" constants (0, 1,
"",true,false) are universally understood and do not need extraction.
The Problem
Your order processing service has grown over two years. Business rules are encoded as raw numbers scattered across 14 files. A product manager asks: "Can we change the free shipping threshold from $50 to $75?" The developer searches for 50 and finds 87 occurrences in the codebase.
// Magic numbers everywhere. What does each one mean?
public class OrderProcessor {
public void process(Order order) {
if (order.total() > 50.0) {
applyFreeShipping(order);
}
if (order.items().size() > 100) {
throw new OrderException("Too many items");
}
if (order.total() > 500.0) {
applyBulkDiscount(order, 10);
}
if (order.total() < 0.01) {
throw new OrderException("Amount too small");
}
scheduleConfirmation(order, 5000);
}
}
What does 50.0 represent? Free shipping threshold? Fraud check limit? Minimum for insurance? What does 10 mean in applyBulkDiscount? Ten percent? Ten dollars? Ten items? The surrounding method may provide clues, but the literal itself does not communicate the domain meaning or unit.
If the free-shipping threshold appears in several files, one update can easily leave a background report or batch job using the old value. The checkout flow and reporting path can then disagree even though both compile successfully.
When you search for 50 in a codebase, you may find port numbers, array sizes, timeout values, and actual dollar amounts. A named domain constant narrows the search and distinguishes the free-shipping threshold from unrelated values such as Thread.sleep(50).
Why It Happens
- Prototype permanence. The developer writes
if (retries > 3)during prototyping, intending to extract a constant "later." That cleanup may never happen before the prototype becomes maintained code. - Context amnesia. The author knows that
86400is seconds-per-day at write time. Six months later, even the author does not recognize it without counting zeros. - Copy-paste propagation. One magic number is copied to a second file, then a third. Each copy is independently maintained (or not), so divergence becomes easier.
- Premature concreteness. The developer treats a retry limit as fixed and hardcodes it, not realizing that different environments or service tiers may need different values.
Magic strings are the same problem
if (status.equals("PENDING")) is a magic string. It has the same risks: no IDE autocompletion, no compile-time checking, and a typo like "PENDNG" silently never matches. Enums solve magic strings the same way named constants solve magic numbers.
How to Detect It
| Signal | Threshold | How to Check |
|---|---|---|
| Domain-specific numeric literals in conditionals | A rule such as if (x > 42) without a named constant | Static analysis rules (Checkstyle MagicNumber) plus review of the value's meaning |
| Same literal in multiple files | The same domain-specific value appears in more than one place | grep -rn "50\.0" src/ then verify whether the occurrences share meaning |
| Literals in method arguments | scheduleRetry(order, 3, 5000) | Review method calls with multiple numeric args |
| Hardcoded timeouts | Thread.sleep(5000) or timeout: 30000 | Search for common timeout patterns |
| String literals in comparisons | if (status.equals("ACTIVE")) | Search for .equals(" patterns |
| Unexplained array/collection sizes | new ArrayList<>(256) without a comment | Check initial capacity arguments |
Review heuristic: if a reader would need to ask "why this specific number, and in what unit?", give the value a name or replace it with a domain type or standard-library expression.
The Fix
The fix has three tiers: named constants for single values, enums for domain sets, and externalized config for runtime-tunable values.
Continue Reading with Premium
Unlock this article and every other in-depth system design guide on the platform with SDEpedia Premium.
Related Articles
Learn why passing raw strings, ints, and booleans for domain concepts creates type-confusion bugs, and how value objects and Java records fix it.
Learn why methods with many parameters invite argument-order bugs, how to detect the smell, and how parameter objects and builders eliminate it.