Avoiding Magic Numbers

23 min read

Clean Code and Readability — review Java numeric literals that hide business or technical meaning behind unnamed thresholds.

1. Introduction

A magic number is a numeric value written directly inside code without a clear name explaining what the value represents.

For example:

JAVA
if (order.getTotalAmount().compareTo(new BigDecimal("5000")) >= 0) {
    order.setShippingCharge(BigDecimal.ZERO);
}

The value 5000 may represent:

  • A free-shipping threshold
  • A premium-order threshold
  • A promotional limit
  • A regulatory limit

The code does not explain which one.

A developer reviewing the method must understand the surrounding business logic before knowing what the number means.

Magic numbers commonly appear in Java projects as:

  • Discount percentages
  • Retry counts
  • Timeouts
  • Maximum limits
  • Minimum thresholds
  • Pagination sizes
  • Batch sizes
  • File-size limits
  • Age limits
  • Interest rates
  • Tax percentages
  • Transaction limits
  • Cache durations
  • Password rules
  • Lockout thresholds

Some numeric literals are naturally understandable.

For example:

JAVA
count + 1

The value 1 clearly means increment by one.

But this code:

JAVA
if (failedAttempts >= 5) {
    lockAccount();
}

raises an important question:

Why five?

If 5 represents an account-lockout policy, it should be expressed using a meaningful name.

Avoiding magic numbers means replacing unexplained numeric business or technical values with named constants, enums, configuration properties, or dedicated value objects where appropriate.

The goal is not to replace every numeric literal.

The goal is to make important values understandable and maintainable.

2. What This Topic Means

In Java development, avoiding magic numbers means ensuring that important numeric values communicate their purpose directly through the code.

Consider:

JAVA
if (customer.getLoyaltyPoints() >= 1000) {
    applyDiscount(customer);
}

A reviewer sees 1000, but the business meaning is unclear.

A better implementation is:

JAVA
private static final int MINIMUM_LOYALTY_POINTS_FOR_DISCOUNT = 1000;

Then:

JAVA
if (customer.getLoyaltyPoints() >= MINIMUM_LOYALTY_POINTS_FOR_DISCOUNT) {
    applyDiscount(customer);
}

Now the condition reads like a business rule.

Magic numbers can appear in several forms.

Business Thresholds

JAVA
if (orderAmount.compareTo(new BigDecimal("5000")) >= 0) {
    ...
}

Timeouts

JAVA
httpClient.setConnectTimeout(30000);

Retry Counts

JAVA
for (int attempt = 0; attempt < 3; attempt++) {
    ...
}

Pagination

JAVA
PageRequest.of(page, 50);

Batch Processing

JAVA
if (records.size() == 1000) {
    flush();
}

Security Rules

JAVA
if (failedLoginAttempts >= 5) {
    lockAccount();
}

Percentage Calculations

JAVA
amount.multiply(new BigDecimal("0.18"));

Date-Based Business Rules

JAVA
LocalDate.now().minusDays(30);

The correct replacement depends on the nature of the value.

Possible approaches include:

  • static final constants
  • Configuration properties
  • Enums
  • Domain objects
  • Dedicated policy classes

A reviewer should choose the simplest option that correctly represents the value.

3. Why It Matters in Real Projects

Readability

Compare:

JAVA
if (failedAttempts >= 5) {
    lockAccount();
}

with:

JAVA
if (failedAttempts >= MAX_FAILED_LOGIN_ATTEMPTS) {
    lockAccount();
}

The second version immediately explains what 5 means.

Developers should not need to remember undocumented values.

Maintainability

Business rules change.

Suppose a fraud service contains:

JAVA
if (transactionAmount.compareTo(new BigDecimal("100000")) > 0) {
    requireManualReview();
}

If the threshold changes to 150000, developers must locate every copy of 100000.

If the value has a meaningful constant:

JAVA
MANUAL_REVIEW_THRESHOLD

the change is easier and safer.

Debugging

Named values improve debugging because logs, conditions, and stack traces can be understood in business terms.

Instead of asking:

Why did this fail at 30?

a developer sees:

JAVA
PAYMENT_EXPIRY_DAYS

and understands what the value controls.

Reliability

Hardcoded values copied across several methods can become inconsistent.

One service may use:

JAVA
5000

while another accidentally uses:

JAVA
5500

for the same business rule.

Team Development

Named constants communicate domain knowledge to other developers.

A junior developer reading:

JAVA
MAXIMUM_REFUND_WINDOW_DAYS

understands the rule immediately.

Performance

Magic numbers are not inherently a performance problem.

However, technical magic numbers such as batch size, timeout, connection pool size, or cache duration can affect performance significantly.

The concern is not that the number is literal.

The concern is that an unexplained value may be difficult to tune correctly.

4. Core Concept

The core idea is:

Important numeric values should communicate their meaning through names or configuration.

Consider:

JAVA
if (order.getTotalAmount().compareTo(new BigDecimal("5000")) >= 0) {
    return BigDecimal.ZERO;
}
return new BigDecimal("200");

There are two important values:

  • 5000
  • 200

Their meaning is not obvious.

A better implementation is:

JAVA
private static final BigDecimal FREE_SHIPPING_THRESHOLD = new BigDecimal("5000.00");
private static final BigDecimal STANDARD_SHIPPING_CHARGE = new BigDecimal("200.00");

Then:

JAVA
if (order.getTotalAmount().compareTo(FREE_SHIPPING_THRESHOLD) >= 0) {
    return BigDecimal.ZERO;
}
return STANDARD_SHIPPING_CHARGE;

The method now documents the rule.

Java Constant Convention

Java constants are normally declared using:

JAVA
private static final

and named using uppercase snake case.

Example:

JAVA
private static final int MAX_RETRY_ATTEMPTS = 3;

Constants vs Configuration

Not every magic number should become a Java constant.

If the business expects a value to change without rebuilding the application, configuration is often more appropriate.

For example:

PROPERTIES
payment.max-retry-attempts=3

may be better than:

JAVA
private static final int MAX_RETRY_ATTEMPTS = 3;

depending on operational requirements.

Constants vs Enums

If a number represents a domain state, an enum may be more appropriate than a numeric constant.

Bad:

JAVA
if (customer.getType() == 2) {
    ...
}

Better:

JAVA
if (customer.getType() == CustomerType.PREMIUM) {
    ...
}

The reviewer should focus on meaning, not simply replacing numbers mechanically.

5. Important Rules

  • Extract important business thresholds into well-named constants.
  • Use configuration when values should change without code deployment.
  • Use enums when numbers represent categories or states.
  • Do not replace obvious values such as 0, 1, or 2 when their meaning is naturally clear.
  • Do not create constants named after the number itself.
  • Prefer business-oriented names.
  • Avoid duplicating the same numeric threshold across multiple classes.
  • Use BigDecimal for important monetary calculations instead of double.
  • Give units to technical constants where ambiguity is possible.
  • Prefer names such as TIMEOUT_SECONDS rather than simply TIMEOUT.
  • Avoid mixing milliseconds, seconds, and minutes without explicit naming.
  • Centralize shared business rules when they represent one authoritative policy.
  • Do not place unrelated constants inside generic utility classes.
  • Avoid exposing implementation-specific numbers directly throughout controllers and services.
  • Test boundary values around important numeric thresholds.
  • Review whether a value belongs in source code, configuration, or a domain policy.

Bad:

JAVA
private static final int FIVE = 5;

Better:

JAVA
private static final int MAX_FAILED_LOGIN_ATTEMPTS = 5;

6. Bad Code Example

Consider a Spring Boot payment service.

JAVA
@Service
public class PaymentService {
    private final PaymentRepository paymentRepository;
    private final FraudClient fraudClient;

    public PaymentService(PaymentRepository paymentRepository, FraudClient fraudClient) {
        this.paymentRepository = paymentRepository;
        this.fraudClient = fraudClient;
    }

    public Payment processPayment(PaymentRequest request) {
        if (request.getAmount().compareTo(new BigDecimal("100000")) > 0) {
            throw new IllegalArgumentException("Payment amount exceeds limit");
        }

        int attempts = 0;

        while (attempts < 3) {
            try {
                FraudDecision decision = fraudClient.check(request);

                if (decision.getRiskScore() > 80) {
                    throw new IllegalStateException("Payment requires manual review");
                }

                Payment payment = new Payment();
                payment.setCustomerId(request.getCustomerId());
                payment.setAmount(request.getAmount());
                payment.setProcessingFee(request.getAmount().multiply(new BigDecimal("0.02")));
                payment.setStatus(PaymentStatus.COMPLETED);

                return paymentRepository.save(payment);
            } catch (FraudServiceUnavailableException ex) {
                attempts++;

                if (attempts == 3) {
                    throw ex;
                }

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException interruptedException) {
                    Thread.currentThread().interrupt();
                    throw new IllegalStateException("Retry interrupted", interruptedException);
                }
            }
        }

        throw new IllegalStateException("Payment processing failed");
    }
}

This code contains several numeric values with important business or technical meaning.

7. Problems in the Bad Code

Payment Limit Is Unclear

This value:

JAVA
100000

represents an important payment rule.

But the reader must infer its meaning from the exception message.

Retry Count Is Repeated

The number 3 appears in:

JAVA
while (attempts < 3)

and:

JAVA
if (attempts == 3)

If one is changed and the other is not, retry behavior becomes inconsistent.

Fraud Threshold Is Hardcoded

This condition:

JAVA
decision.getRiskScore() > 80

does not explain why 80 is important.

Processing Fee Is Hidden

This calculation:

JAVA
request.getAmount().multiply(new BigDecimal("0.02"))

contains a business fee rate without a name.

Retry Delay Has No Unit

This call:

JAVA
Thread.sleep(2000);

uses milliseconds, but the value does not communicate that clearly.

Maintainability Risk

Any future change requires editing implementation code directly.

Configuration Opportunity

Values such as:

  • Maximum payment amount
  • Fraud threshold
  • Retry attempts
  • Retry delay

may need operational configuration rather than hardcoded constants.

Testing Is Harder to Understand

A test using 80, 100000, or 3 must know the same undocumented assumptions.

8. Code Review Findings

A senior reviewer should identify:

  • 100000 appears to represent a maximum payment threshold.
  • 3 controls retry behavior and is duplicated.
  • 80 represents the fraud/manual-review threshold.
  • 0.02 represents a processing fee.
  • 2000 is a retry delay expressed in milliseconds.
  • Each value should have a meaningful name.
  • Operational values may belong in configuration rather than Java constants.
  • Money values should remain BigDecimal.
  • Units should be explicit in names.
  • Tests should cover the exact boundaries around these thresholds.
  • Retry behavior might be better implemented through an existing retry mechanism if the project already uses one, but a new framework should not be introduced solely to remove magic numbers.

The reviewer should also determine whether all values have the same ownership.

For example:

  • Fee rate may be a business-policy value.
  • Retry delay may be an infrastructure value.
  • Fraud threshold may belong to fraud configuration.

They should not automatically be placed in one generic constants class.

9. Reviewer Comment Example

100000 looks like a business payment limit. Could we give this threshold a meaningful name or move it to configuration if operations need to change it without deployment?

The retry count 3 appears in more than one place. Please centralize it so both checks cannot diverge.

Can we name the fraud threshold rather than comparing directly with 80? That would make the review rule easier to understand.

0.02 appears to be the processing-fee rate. Please extract it into a business-oriented constant or configuration property.

Thread.sleep(2000) hides the time unit. Please make the retry delay explicit, for example RETRY_DELAY_MILLIS, or use a Duration.

I would avoid moving all of these values into a generic Constants class. They belong to different business and infrastructure concerns.

10. Improved Code

JAVA
@Service
public class PaymentService {
    private static final BigDecimal MAXIMUM_PAYMENT_AMOUNT = new BigDecimal("100000.00");
    private static final BigDecimal PROCESSING_FEE_RATE = new BigDecimal("0.02");
    private static final int MANUAL_REVIEW_RISK_SCORE = 80;
    private static final int MAX_FRAUD_CHECK_ATTEMPTS = 3;
    private static final Duration FRAUD_RETRY_DELAY = Duration.ofSeconds(2);

    private final PaymentRepository paymentRepository;
    private final FraudClient fraudClient;

    public PaymentService(PaymentRepository paymentRepository, FraudClient fraudClient) {
        this.paymentRepository = paymentRepository;
        this.fraudClient = fraudClient;
    }

    public Payment processPayment(PaymentRequest request) {
        validatePaymentAmount(request.getAmount());

        int attempts = 0;

        while (attempts < MAX_FRAUD_CHECK_ATTEMPTS) {
            try {
                FraudDecision decision = fraudClient.check(request);
                validateFraudDecision(decision);

                Payment payment = createPayment(request);

                return paymentRepository.save(payment);
            } catch (FraudServiceUnavailableException ex) {
                attempts++;

                if (attempts >= MAX_FRAUD_CHECK_ATTEMPTS) {
                    throw ex;
                }

                waitBeforeRetry();
            }
        }

        throw new IllegalStateException("Payment processing failed");
    }

    private void validatePaymentAmount(BigDecimal amount) {
        if (amount.compareTo(MAXIMUM_PAYMENT_AMOUNT) > 0) {
            throw new IllegalArgumentException("Payment amount exceeds limit");
        }
    }

    private void validateFraudDecision(FraudDecision decision) {
        if (decision.getRiskScore() > MANUAL_REVIEW_RISK_SCORE) {
            throw new IllegalStateException("Payment requires manual review");
        }
    }

    private Payment createPayment(PaymentRequest request) {
        Payment payment = new Payment();
        payment.setCustomerId(request.getCustomerId());
        payment.setAmount(request.getAmount());
        payment.setProcessingFee(request.getAmount().multiply(PROCESSING_FEE_RATE));
        payment.setStatus(PaymentStatus.COMPLETED);
        return payment;
    }

    private void waitBeforeRetry() {
        try {
            Thread.sleep(FRAUD_RETRY_DELAY.toMillis());
        } catch (InterruptedException ex) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("Retry interrupted", ex);
        }
    }
}

If the values should be configurable:

JAVA
@ConfigurationProperties(prefix = "payment")
public record PaymentProperties(
        BigDecimal maximumAmount,
        BigDecimal processingFeeRate,
        int manualReviewRiskScore,
        int maxFraudCheckAttempts,
        Duration fraudRetryDelay) {
}

Example configuration:

PROPERTIES
payment.maximum-amount=100000.00
payment.processing-fee-rate=0.02
payment.manual-review-risk-score=80
payment.max-fraud-check-attempts=3
payment.fraud-retry-delay=2s

11. Improved Code Explanation

Maximum Payment Amount Is Named

Instead of:

JAVA
100000

the code uses:

JAVA
MAXIMUM_PAYMENT_AMOUNT

The condition now communicates the business rule.

Processing Fee Has Meaning

The value:

JAVA
0.02

is represented as:

JAVA
PROCESSING_FEE_RATE

A developer immediately knows that it is a percentage-like fee rate.

Fraud Threshold Is Explicit

Instead of:

JAVA
riskScore > 80

the code uses:

JAVA
riskScore > MANUAL_REVIEW_RISK_SCORE

Retry Count Has One Source of Truth

Both retry conditions reference:

JAVA
MAX_FRAUD_CHECK_ATTEMPTS

Changing retry policy requires one modification.

Duration Represents Time Clearly

Instead of:

JAVA
2000

the code uses:

JAVA
Duration.ofSeconds(2)

This makes the unit explicit and reduces unit-conversion mistakes.

Configuration Is an Option

If operations or product teams frequently change thresholds, configuration properties may be more appropriate than static final constants.

12. Bad Code vs Improved Code

AspectBad CodeImproved Code
ReadabilityNumbers require interpretationNames explain business meaning
MaintainabilityValues changed directly in logicValues centralized
ReliabilityRepeated literals can divergeOne authoritative value
TestabilityTest values depend on hidden assumptionsBoundaries map to named rules
ConfigurationRedeployment required for every changeSelected values can be externalized
Time units2000 is ambiguousDuration makes unit explicit
PR ReviewReviewer must infer numeric meaningReviewer can review business intent directly

13. Real Project Scenario

Consider a healthcare appointment platform.

A scheduling service contains:

JAVA
if (appointmentDate.isBefore(LocalDate.now().plusDays(2))) {
    throw new IllegalArgumentException("Appointment must be booked in advance");
}

if (patient.getMissedAppointments() >= 3) {
    requireManualApproval();
}

if (consultationDuration > 60) {
    chargeExtendedConsultationFee();
}

The numbers 2, 3, and 60 represent different business policies:

  • Minimum booking lead time
  • Missed-appointment threshold
  • Maximum standard consultation duration

Months later, the business changes:

  • Booking lead time from 2 days to 1 day
  • Manual review threshold from 3 missed appointments to 2
  • Standard consultation duration from 60 minutes to 45

If the values are scattered across:

  • REST controllers
  • Services
  • Batch jobs
  • Notification logic

the change becomes risky.

A better design uses explicit policy values:

JAVA
private static final int MINIMUM_BOOKING_LEAD_DAYS = 2;
private static final int MAX_MISSED_APPOINTMENTS_WITHOUT_REVIEW = 3;
private static final Duration STANDARD_CONSULTATION_DURATION = Duration.ofMinutes(60);

If product teams need to adjust these values frequently, they may belong in configuration or a policy service.

14. Production Impact

Magic numbers can create production issues when they represent critical rules.

Inconsistent Business Results

If the same threshold is duplicated across services, one copy may be updated while another remains unchanged.

Example:

  • API accepts payments up to ₹1,50,000.
  • Batch validation still rejects anything above ₹1,00,000.

Incorrect Boundary Behavior

A developer may not understand whether:

JAVA
100

represents:

  • Maximum inclusive value
  • Maximum exclusive value
  • Percentage
  • Currency amount
  • Number of records

This can cause incorrect comparisons.

Timeout Misconfiguration

Using:

JAVA
30000

without a unit can lead to confusion between:

  • Milliseconds
  • Seconds
  • Microseconds

Wrong Financial Calculations

A fee or tax rate such as:

JAVA
0.18

may be duplicated and changed inconsistently.

Difficult Production Support

Logs and code reviews become harder when engineers must decode numeric values during incidents.

Deployment Dependency

If frequently changing operational values are hardcoded, even minor adjustments require:

  • Code change
  • Pull Request
  • Build
  • Deployment

External configuration may reduce that operational burden where appropriate.

15. Common Developer Mistakes

Extracting Every Number

Developers sometimes create:

JAVA
private static final int ZERO = 0;
private static final int ONE = 1;

This usually adds noise rather than meaning.

Poor Constant Names

Bad:

JAVA
private static final int LIMIT = 5;

Better:

JAVA
private static final int MAX_FAILED_LOGIN_ATTEMPTS = 5;

Generic Constant Classes

Example:

JAVA
public class Constants {
    public static final int FIVE = 5;
    public static final int THIRTY = 30;
    public static final BigDecimal RATE = new BigDecimal("0.18");
}

This removes numeric literals but does not add business meaning.

Copying Constants Across Classes

Two services define different constants for the same business rule.

Hardcoding Time in Milliseconds

Example:

JAVA
Thread.sleep(60000);

The unit is easy to misread.

Using double for Money

Bad:

JAVA
double taxRate = 0.18;

For monetary calculations, BigDecimal is usually safer.

Putting Everything in Configuration

Not every constant needs to be externally configurable.

Some values are stable implementation details.

Ignoring Units

Bad:

JAVA
private static final int TIMEOUT = 30;

Better:

JAVA
private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30);

Numeric Status Codes Inside Business Logic

Bad:

JAVA
if (customer.getStatus() == 2) {
    ...
}

Use an enum or meaningful type.

16. Edge Cases

Naturally Obvious Numbers

This code is usually fine:

JAVA
for (int index = 0; index < items.size(); index++) {
    ...
}

Extracting 0 into:

JAVA
START_INDEX

would not improve readability.

Mathematical Constants

Values used in domain formulas may sometimes be naturally understood but still deserve names if business-specific.

Percentage Representation

A rate may be represented as:

JAVA
0.18

or:

JAVA
18

The naming should make the representation clear.

Examples:

JAVA
TAX_RATE

or:

JAVA
TAX_PERCENTAGE

Units

A value such as:

JAVA
30

could mean:

  • 30 milliseconds
  • 30 seconds
  • 30 days
  • 30 records

Use types like Duration when appropriate.

Currency

A threshold may differ by currency.

One global constant may be incorrect if the application supports multiple currencies.

Regional Business Rules

Legal age or transaction limits may differ by jurisdiction.

A single hardcoded constant may not be enough.

Feature-Specific Configuration

A threshold may intentionally vary across environments.

For example, test and production rate limits may be different.

Integer Overflow

Large numeric constants should use appropriate types such as long.

17. Performance Considerations

Avoiding magic numbers generally has no meaningful runtime performance cost.

Using:

JAVA
MAX_RETRY_ATTEMPTS

instead of:

JAVA
3

does not materially affect performance.

Java compile-time constants are highly optimized.

However, the values themselves may affect runtime behavior.

Batch Size

JAVA
BATCH_SIZE

can influence:

  • Memory usage
  • Database round trips
  • Processing throughput

Timeout

JAVA
REQUEST_TIMEOUT

can affect:

  • Thread utilization
  • User latency
  • Downstream pressure

Retry Count

JAVA
MAX_RETRY_ATTEMPTS

can affect:

  • Downstream traffic
  • Recovery behavior
  • Failure amplification

Connection Pool Size

A numeric pool setting directly affects concurrency and resource consumption.

Page Size

Large page sizes may increase memory use and response time.

Therefore, reviewers should ask whether technical numeric values are:

  • Named
  • Correctly scoped
  • Properly configured
  • Tunable where necessary

The naming change itself is not a performance optimization.

18. Security Considerations

Magic numbers are not inherently security vulnerabilities.

However, important security thresholds should be explicit.

Login Attempt Limits

Bad:

JAVA
if (failedAttempts >= 5) {
    lockAccount();
}

Better:

JAVA
if (failedAttempts >= MAX_FAILED_LOGIN_ATTEMPTS) {
    lockAccount();
}

Token Expiration

Bad:

JAVA
expiration = now.plusSeconds(3600);

Better:

JAVA
expiration = now.plus(ACCESS_TOKEN_TTL);

Password Length

Bad:

JAVA
if (password.length() < 8) {
    ...
}

Better:

JAVA
if (password.length() < MINIMUM_PASSWORD_LENGTH) {
    ...
}

Rate Limits

Values controlling requests per minute should not be scattered across endpoints.

Security Configuration

Some security limits may be better externalized into controlled configuration.

However, allowing arbitrary runtime modification of security policies may itself require governance.

Reviewer Responsibility

The reviewer should understand the purpose of security-related numbers and ensure they are not accidentally duplicated or inconsistent.

19. Testing Considerations

Important numeric values usually create boundaries.

Boundary testing is therefore especially important.

Suppose:

JAVA
private static final BigDecimal MAXIMUM_PAYMENT_AMOUNT = new BigDecimal("100000.00");

Test:

  • 99999.99
  • 100000.00
  • 100000.01

Example:

JAVA
@Test
void shouldAllowPaymentAtMaximumAmount() {
    PaymentRequest request = requestWithAmount(new BigDecimal("100000.00"));

    assertDoesNotThrow(() -> paymentService.processPayment(request));
}

And:

JAVA
@Test
void shouldRejectPaymentAboveMaximumAmount() {
    PaymentRequest request = requestWithAmount(new BigDecimal("100000.01"));

    assertThrows(
            IllegalArgumentException.class,
            () -> paymentService.processPayment(request));
}

Retry Tests

Verify:

  • First attempt succeeds
  • Second attempt succeeds
  • Maximum retry count reached
  • No additional attempt after the maximum

Risk Threshold Tests

Test:

  • Score below threshold
  • Score exactly at threshold
  • Score above threshold

Fee Tests

Verify monetary calculations using precise BigDecimal assertions.

Configuration Tests

If values come from configuration, verify property binding.

Unit Tests Should Express Meaning

Tests should preferably use the same domain terminology as production code rather than unexplained literals.

20. Refactoring Guidelines

Step 1: Identify Important Numeric Literals

Search business and infrastructure code for numeric values that represent rules.

Step 2: Understand Their Meaning

Do not extract a number before understanding what it represents.

Step 3: Check for Duplication

Search the project for the same or equivalent value.

Step 4: Determine Ownership

Ask whether the value belongs to:

  • The current class
  • A domain policy
  • Configuration
  • An enum
  • Infrastructure settings

Step 5: Introduce a Meaningful Name

Before:

JAVA
if (attempts >= 3) {
    ...
}

After:

JAVA
if (attempts >= MAX_RETRY_ATTEMPTS) {
    ...
}

Step 6: Preserve Behavior

Do not change the number while refactoring unless the business requirement also changes.

Step 7: Add Boundary Tests

Verify behavior immediately below, at, and above the threshold.

Step 8: Replace All Authoritative Copies

If several locations implement the same rule, centralize them carefully.

Step 9: Externalize Only When Necessary

Do not turn every number into a configuration property.

Step 10: Review Naming

A constant name should explain the rule without requiring the developer to inspect its value.

21. Best Practices

  • Name numeric business rules explicitly.
  • Keep constants close to the domain they belong to.
  • Use private static final for class-level constants when appropriate.
  • Use configuration for operationally adjustable values.
  • Use enums instead of numeric state codes.
  • Use Duration for time-based values.
  • Use BigDecimal for precise monetary rules.
  • Include units in names when types cannot express them.
  • Avoid duplicating thresholds across services.
  • Test numeric boundaries.
  • Keep constants cohesive.
  • Avoid generic constants classes.
  • Keep one source of truth for shared business values.
  • Document unusual formulas when naming alone is insufficient.
  • Prefer types that encode meaning over primitive numbers where practical.

22. Practices to Avoid

Unexplained Business Numbers

Avoid:

JAVA
if (age >= 18) {
    ...
}

when 18 represents a business or legal eligibility rule used in multiple places.

Constants Named After Values

Avoid:

JAVA
private static final int THIRTY = 30;

Generic Names

Avoid:

JAVA
private static final int LIMIT = 10;

Large Common Constants Classes

Avoid unrelated values such as:

JAVA
PAYMENT_LIMIT
MAX_LOGIN_ATTEMPTS
ORDER_PAGE_SIZE
CACHE_TTL

inside one global class solely for reuse.

Hardcoded Milliseconds

Avoid:

JAVA
Thread.sleep(5000);

when a Duration would communicate intent.

Numeric Domain States

Avoid:

JAVA
status == 1

for business states.

Duplicate Thresholds

Avoid defining the same business value in multiple services.

Premature Configuration

Do not make stable internal values externally configurable without a genuine operational need.

Imprecise Money Calculations

Avoid using double where financial precision matters.

23. Code Review Checklist

  • Does this numeric literal have an obvious meaning?
  • Does the number represent a business rule?
  • Does the number represent an infrastructure setting?
  • Would a named constant improve readability?
  • Should the value be configurable?
  • Is the same numeric value duplicated elsewhere?
  • Does the same number represent the same concept everywhere?
  • Is the constant name business-oriented?
  • Does the name include a unit where needed?
  • Would Duration be clearer than a raw integer?
  • Should this numeric state be replaced by an enum?
  • Is BigDecimal being used for monetary values?
  • Is the business threshold owned by the correct class or component?
  • Are unrelated constants being placed in a generic constants class?
  • Are boundary values covered by tests?
  • Is the threshold inclusive or exclusive?
  • Is the configured value validated?
  • Could different regions or currencies require different values?
  • Is this value expected to change frequently?
  • Would external configuration reduce unnecessary deployments?
  • Does the refactoring preserve existing business behavior?
  • Is the resulting code easier to understand than the original?

24. Common Pull Request Review Comments

  1. *What does 5000 represent here? If this is the free-shipping threshold, please give it a business-oriented name.*
  1. *The retry count 3 appears in multiple conditions. Can we centralize it so the retry policy has one source of truth?*
  1. *Please avoid the raw 30000 timeout. A Duration would make the unit and intent clearer.*
  1. *0.18 appears to be the tax rate. Please extract it into the appropriate pricing or tax policy rather than leaving the value inline.*
  1. *This value looks operational rather than fixed business logic. Should it come from configuration instead of being hardcoded?*
  1. *I would avoid adding this to the global Constants class. The value belongs specifically to payment processing.*
  1. *Can we add boundary tests around this threshold? We should verify behavior below, exactly at, and above the limit.*
  1. *The status comparison uses 2. Could we model this as an enum so the code communicates the actual state?*
  1. *The name LIMIT is too generic. Please use a name that describes what is being limited.*
  1. *This monetary calculation uses double. Since this is a financial rule, please consider BigDecimal with an explicit rate.*

25. Code Review Exercise

Review the following Spring Boot account service.

Identify:

  • Problems
  • Code smells
  • Risks
  • Improvements

Do not read the solution until completing your review.

JAVA
@Service
public class AccountService {
    private final AccountRepository accountRepository;
    private final NotificationService notificationService;

    public AccountService(AccountRepository accountRepository, NotificationService notificationService) {
        this.accountRepository = accountRepository;
        this.notificationService = notificationService;
    }

    public void recordFailedLogin(Long accountId) {
        Account account = accountRepository.findById(accountId)
                .orElseThrow(() -> new IllegalArgumentException("Account not found"));

        int attempts = account.getFailedLoginAttempts() + 1;
        account.setFailedLoginAttempts(attempts);

        if (attempts >= 5) {
            account.setLocked(true);
            account.setLockedUntil(Instant.now().plusSeconds(1800));
        }

        accountRepository.save(account);

        if (attempts == 3) {
            notificationService.sendSecurityWarning(account.getUserId());
        }
    }

    public boolean canResetPassword(Account account) {
        return account.getPasswordAgeDays() >= 90
                || account.getFailedLoginAttempts() >= 5;
    }
}

Review the code for:

  • Repeated thresholds
  • Security-policy values
  • Time units
  • Naming
  • Boundary behavior
  • Policy ownership
  • Configuration opportunities

26. Exercise Solution

Problems Identified

1. Account Lockout Threshold Is Hardcoded

The value:

JAVA
5

controls account locking.

It is a security policy and should have an explicit name.

2. Same Threshold Is Repeated

The value 5 appears in:

JAVA
attempts >= 5

and:

JAVA
account.getFailedLoginAttempts() >= 5

If one changes, the other may not.

3. Warning Threshold Is Unclear

The value:

JAVA
3

represents the point at which a warning is sent.

4. Lock Duration Is a Magic Number

This:

JAVA
1800

means 30 minutes in seconds.

The code does not make that obvious.

5. Password Age Is Hardcoded

The value:

JAVA
90

represents a password-age policy.

6. Security Policies Are Mixed Into Implementation

These values may need centralized security-policy ownership.

Improved Code

JAVA
@Service
public class AccountService {
    private static final int MAX_FAILED_LOGIN_ATTEMPTS = 5;
    private static final int SECURITY_WARNING_ATTEMPT_THRESHOLD = 3;
    private static final int MAX_PASSWORD_AGE_DAYS = 90;
    private static final Duration ACCOUNT_LOCK_DURATION = Duration.ofMinutes(30);

    private final AccountRepository accountRepository;
    private final NotificationService notificationService;
    private final Clock clock;

    public AccountService(AccountRepository accountRepository, NotificationService notificationService, Clock clock) {
        this.accountRepository = accountRepository;
        this.notificationService = notificationService;
        this.clock = clock;
    }

    public void recordFailedLogin(Long accountId) {
        Account account = accountRepository.findById(accountId)
                .orElseThrow(() -> new IllegalArgumentException("Account not found"));

        int attempts = account.getFailedLoginAttempts() + 1;
        account.setFailedLoginAttempts(attempts);

        if (attempts >= MAX_FAILED_LOGIN_ATTEMPTS) {
            lockAccount(account);
        }

        accountRepository.save(account);

        if (attempts == SECURITY_WARNING_ATTEMPT_THRESHOLD) {
            notificationService.sendSecurityWarning(account.getUserId());
        }
    }

    public boolean canResetPassword(Account account) {
        return account.getPasswordAgeDays() >= MAX_PASSWORD_AGE_DAYS
                || account.getFailedLoginAttempts() >= MAX_FAILED_LOGIN_ATTEMPTS;
    }

    private void lockAccount(Account account) {
        account.setLocked(true);
        account.setLockedUntil(Instant.now(clock).plus(ACCOUNT_LOCK_DURATION));
    }
}

Why Each Change Is Useful

  • Security thresholds now have meaningful names.
  • Lockout policy has one source of truth.
  • Duration communicates the lock period directly.
  • Password-age policy is understandable without inspecting the number.
  • Injecting Clock makes time-based tests easier.
  • A policy change no longer requires searching for unexplained literals.

Configuration Option

If security administrators must modify these values without deployment, they may belong in configuration:

JAVA
@ConfigurationProperties(prefix = "security.account")
public record AccountSecurityProperties(
        int maxFailedLoginAttempts,
        int warningAttemptThreshold,
        int maxPasswordAgeDays,
        Duration lockDuration) {
}

However, externalizing security policy should be intentional and controlled.

27. Interview Perspective

Magic-number questions are common in Java code-review interviews because they reveal whether a candidate understands maintainability beyond syntax.

Java Interview

A candidate may receive:

JAVA
if (attempts >= 5) {
    lock();
}

and be asked how it should be improved.

A basic answer is:

Create a constant.

A stronger answer is:

First determine what 5 represents. If it is the maximum failed-login threshold, use a meaningful constant such as MAX_FAILED_LOGIN_ATTEMPTS. If it needs operational tuning, consider configuration.

Spring Boot Interview

The interviewer may ask:

Should every business number become an application.properties value?

A strong candidate should say no.

Configuration is useful when values need:

  • Environment-specific behavior
  • Runtime operational tuning
  • Product configuration without rebuilding

Stable implementation constants may remain in code.

Senior Developer Interview

A senior developer may be asked where constants should live.

The correct answer depends on ownership.

Avoid putting everything inside:

JAVA
CommonConstants

Instead, keep values near:

  • Pricing policy
  • Security policy
  • Payment configuration
  • Order rules
  • Infrastructure settings

Code Review Interview

Candidates may be expected to recognize that numeric literals are not automatically wrong.

For example:

JAVA
index + 1

does not need:

JAVA
INDEX_INCREMENT

Good review judgment matters more than mechanically removing every number.

28. Interview Questions and Answers

Basic Question

Question: What is a magic number in Java?

Answer:

A magic number is a numeric literal used directly in code without clearly communicating its meaning. For example, failedAttempts >= 5 is harder to understand than failedAttempts >= MAX_FAILED_LOGIN_ATTEMPTS.

Intermediate Question

Question: Should every numeric literal be converted into a constant?

Answer:

No. Values such as 0 and 1 are often naturally understandable in loops, increments, and comparisons. A numeric literal should usually be extracted when it represents business knowledge, a technical policy, a threshold, a timeout, a percentage, or another value whose meaning is not obvious.

Advanced Question

Question: When should a magic number become configuration instead of a Java constant?

Answer:

Use configuration when the value is expected to vary by environment, change operationally, or be modified without rebuilding the application. Use a Java constant when the value is stable and naturally belongs to the implementation or domain code. The decision should be based on ownership and change frequency.

Scenario-Based Question

Question: A transaction limit of 100000 appears in three microservices. What would you do?

Answer:

First, I would determine whether all three values represent the same business rule. If they do, the rule needs one authoritative ownership model. Depending on the architecture, that might be a domain service, central configuration, policy service, or shared domain contract. I would not simply create three constants with the same value because that still duplicates business knowledge.

Code-Review Question

Question: What would you comment on this code?

JAVA
Thread.sleep(30000);

Answer:

I would ask for a meaningful time representation, preferably a Duration, because 30000 hides the unit and intent. For example, RETRY_DELAY = Duration.ofSeconds(30) communicates the purpose clearly.

Real-Project Question

Question: How have you handled magic numbers in financial applications?

Answer:

I separate stable implementation values from business-configurable values. Monetary thresholds and rates are represented using BigDecimal, named according to the actual business rule, and tested at boundaries. Values that product or operations teams need to adjust are externalized into validated configuration or policy components rather than being scattered across services.

29. Quick Rule to Remember

If a reviewer has to ask “What does this number mean?”, the number probably needs a name.

30. Final Takeaway

What the Developer Should Remember

Magic numbers hide important information.

The problem is not that numeric literals exist.

The problem is that important numbers appear without explaining their purpose.

Instead of:

JAVA
if (attempts >= 5) {
    ...
}

prefer:

JAVA
if (attempts >= MAX_FAILED_LOGIN_ATTEMPTS) {
    ...
}

Developers should:

  • Name business thresholds.
  • Name technical limits.
  • Use configuration when values need operational control.
  • Use Duration for time-related values.
  • Use BigDecimal for monetary calculations.
  • Use enums instead of numeric state codes.
  • Keep constants near their business owner.
  • Avoid generic constants classes.
  • Test important boundaries.
  • Avoid extracting naturally obvious numbers unnecessarily.

What the Reviewer Should Check

During Pull Request review, ask:

  • What does this number represent?
  • Is the meaning obvious?
  • Is the value duplicated?
  • Is it a business rule?
  • Is it an operational setting?
  • Should it be configurable?
  • Does the name describe the rule?
  • Are units explicit?
  • Is money represented precisely?
  • Is an enum more appropriate?
  • Are boundary values tested?
  • Does this rule have one authoritative owner?

What Should Be Avoided in Production Code

Avoid unexplained values such as:

JAVA
3
5
30
80
1000
5000
0.02

when they control important application behavior.

Avoid replacing them with meaningless constants such as:

JAVA
THREE
FIVE
VALUE_30
DEFAULT_NUMBER

Avoid putting unrelated constants into one global utility class.

Avoid hardcoding frequently changing operational settings when configuration is more appropriate.

Avoid assuming that every numeric literal is a code smell.

The production-quality approach is:

Give important numbers meaningful business or technical names, put them in the correct ownership boundary, and make every significant threshold understandable without forcing the next developer to guess.