Don’t create constants.

Aren’t magic numbers (or strings, etc.) a bad practice? Creating constants can be a symptom of underlying anti-patterns; they treat the symptom, not the cause.

Luís Soares
CodeX
8 min readJan 15, 2021

--

Photo by Alexandros Tsiambartas on Pinterest

“But I can use them in the tests!”

This is the worst reason to create constants. Tests should know very little about the implementation.

⚠️ Tests should not be coupled to implementation details like constants! Tests should only care about behaviors that are exercised through APIs.

“I need to read its value in the test”

Initially, a particular constant was local/private, but you promoted it to global for testing. This was a bad idea because the tests are now coupled with the implementation. Tests should be seen as clients of the implementation (i.e., outsiders), so they should only know about its public interface.

⚠️ Changing the visibility of implementation classes, methods, constants, etc., for testing is an anti-pattern.

🚨
assertEquals("T1000", json.at(JsonConstants.MODEL_PROP).textValue())


assertEquals("T1000", json.at("model").textValue())

On the other hand, when a constant is shared between implementation and tests, you could change its value to a wrong one, and no test would fail. The right thing to do is to hardcode the literal in the tests, which goes in line with the Evident Data test pattern (and DAMP):

Evident Data seems to be an exception to the rule that you don’t want magic numbers in your code. Test-Driven Development

In short, tests should provide a safety net for errors when altering a constant rather than relying on shared value.

“I need to change its value in the test”

If you change a constant value in runtime, it’s no longer constant, right? They’re global mutable variables (a nasty antipattern). The correct thing to do is to inject the value into the component that needs it, which reduces the scope and makes dependencies evident:

🚨
ComponentUnderTest.MAX_RETRIES = 5
componentUnderTest = ComponentUnderTest()

componentUnderTest = ComponentUnderTest(maxRetries = 5)

“But they’re used in multiple places”

So now you got rid of the promiscuous sharing between implementation and tests, but you still want your constants shared because they’re needed in multiple places. First, confirm if they are used in multiple places. I often see one-offs that could be easily inlined.

Because distinct features share the same values

Shared constants are similar to public static utilities as they couple everything together. Changing a constant value can have unpredictable consequences in all the places where it’s used. You always feel uneasy when changing them.

[…] Constants.CRLF remains alone in a global scope of visibility, without any semantic usage around it. We simply don't know how this object is used, in what context, and how the changes we may make will affect its users. Elegant Objects

Let’s say you have an error message like “Client is inactive” at a few web handlers. Do you need to share it across multiple places? Why? So you DRY? But each web handler has its own identity and will evolve independently!

Not all code duplication is knowledge duplication. The Pragmatic Programmer

Because a particular feature or component is split (but should not)

You have a code smell when a technical (or even business) decision impacts multiple places in the code. Some decisions are meant to be atomic, meaning a decision in the code and its implications should be co-located. Items that work closely together should be placed next to each other. By sharing constants, are you trying to hide that smell? For example, a shared constant of a database table name hints about functionality that should be together in the first place.

Stop sharing code. Sharing constants might be a smell of low code cohesion because they bind separate components together:

⚠️ A and B show low cohesion. This hints that they are the same thing or share a concept that could be isolated and shared through dependency injection.

📝 If you have constants that are part of a component’s public interface (e.g., a React component that has the states LOADING and ERROR or a server-side filter by field name), you might have a valid case for constants.

“But I can centralize all the constants”

Do you have a file for storing constants? Are you trying to centralize some configuration? GitHub is riddled with examples of DB table/field names, UI strings, JSON field names, etc. These files remind me of files for exceptions or files full of enums. Why not have a file for variables and another for all functions? It’s like setting your dining table by putting all the forks in one place and all the knives in another.

⚠️ Don’t create a file full of constants! Just hardcode them where they belong. You never know the blast radius of changing a file full of constants.

Files with constants are like glue for independent features — code hotspots. Why can the whole codebase see all constants if each matters to a single place? Constants are details of a particular feature or component, so move them to their logical owners and make them private, thus encapsulating them.

⚠️ A file you keep changing for multiple reasons (e.g., files with constants) likely violates the Single Responsibility Principle.

Constants like table/field names, JSON keys, and error messages help you configure particular places of your code. Constants with broader scopes than required promote over-sharing (and work badly as implicit documentation). Files with constants are a smell; you should not need to share them in the first place. Instead, code should be organized by feature or component rather than technical category.

For the things that can change and must be configurable, use dependency injection and centralize them in the wiring section of your app.

“I can change values easily”

You may have private constants defined where they’re needed (which is way better than files of public constants), but I’d still argue against that, given that they are placed away from where they’re needed. Separating definition from usage reminds me of old-fashioned C, where we defined all variables at the top. Why do you force me to scroll up and down to understand one line of code? Why can the whole file see a constant if it matters to a single place (the smaller the scope/visibility of things, the better)?

In physics, the principle of locality states that an object is influenced directly only by its immediate surroundings. Principle of locality

You may argue that it’s nice to change values at any time, but why do you want to let something be configurable that you’ll likely never change? Don’t make static things configurable (or you’re lying to the reader). If the configuration is supposed to be dynamic, you should consider dependency injection. If a value rarely changes, it’s way safer to put it where it’s needed, where you know exactly what it’ll impact.

“But they are standard”

“What about Pi or HTTP status codes?” Well, don’t create those constants yourself. These are actual constants since they have a standard agreed meaning. This means they probably belong to the runtime (e.g., PI) or some library you already have (e.g., HTTP status codes in Apache HttpCore). You may argue we’re creating coupling with a library, to which I’d say this coupling should only exist where you already use the library (e.g., an adapter).

bvHTTP status codes

“But they improve readability!”

Do they? Consider the example:

const val UPLOADER_TIMEOUT = 30_000
// ...
fileUploader.setTimeout(UPLOADER_TIMEOUT)

🆚

fileUploader.setTimeout(30_000)

You can easily see that we’re setting the file upload timeout to 30 seconds. Also, there’s no distance between definition and usage. Why the constant? Check the following example of a value object:

data class Password(val password: String) {
init {
require(password.length() > 7)
}
}

Isn’t it clear that the password length must be greater than 7? It doesn’t look magical to me. Creating a constant will bring visual load, splitting the definition from the usage. Worse, it will promote the temptation to share it. Hardcoding the number looks pretty reasonable.

Now, let’s see a method that obtains a user profile from an external API:

fun fetchProfile(id: String): Profile {
val httpRequest = HttpRequest.newBuilder()
.uri(URI.create("$apiUrl/profiles/$id"))
.GET()
return newHttpClient.send(httpRequest.build(), ofString()).run {
check(statusCode() == HttpStatus.OK)
body().toProfile()
}
}

Is it worth storing “profiles” in a constant? I don’t think so. The snippet above contains, cohesively, all I need to understand it. Even if it were more complex, I’d expect the enclosing method to be small and provide meaning. Commonly, constants bring more visual load without benefits.

What about repeating JSON keys at an object serialization and deserialization? Wouldn’t you want to store them in constants to avoid issues? Well, tests provide me safety, not shared code. Therefore, I don’t mind repeating the literal to make the code self-evident.

“But they are magical”

A good reason to create constants is to provide meaning to concepts like mapping low-level codes. Representing an “element not found” with the constant NOT_FOUND rather than -1 prevents repetition and magic. In those cases, ensure they’re cohesive by placing them in their owner (rather than an error-like file). Also, make them private; share them only if they’re supposed to be part of some public interface.

Another example is when you want to give meaning to an arbitrary value (e.g., the number of milliseconds per week). Do it only if it’s not clear, especially considering the context. If so, consider a local/private constant or method to hold the magic number.

Conclusion

Constantly challenge the need for constants. They are implementation details. They create a coupling between definition and usage, harming code cohesion. If you need them, ask first if you’re trying to hide a code smell by using constants to…

🚨 Share values between implementation and tests;
🚨 Share values between features;
🚨 Bridge parts of code that should be together in the first place;
🚨 Centralize control in a single place.

There are some reasonable use cases for constants, though:

✅ To build semantics on an API’s language;
✅ To encode actual constants (e.g., Avogadro constant, ANSI color codes) — but check the runtime contains them (or if it makes sense to add a library);
✅ To encode magic values like low-level codes (e.g., C error codes).

In short, if A and B share a constant, they probably should be the same thing, or if they aren’t, they should have the values hardcoded instead if the context suffices.

--

--

Luís Soares
CodeX

I write about automated testing, Lean, TDD, CI/CD, trunk-based dev., user-centric dev, domain-centric arch, coding good practices,