Constants #
Every program has values that must not change during execution: endpoint URLs, maximum login attempt limits, database names, standard error codes. Storing these values as literals scattered across the code — often called magic numbers or magic strings — is a dangerous practice. One value change forces you to search and replace in many places, with the risk of missing one and creating inconsistencies. Kotlin provides two mechanisms for constants: val for values determined at runtime, and const val for values that are certain from compile time. Understanding the difference between the two — and when to use which — is an important part of writing clean, efficient Kotlin code.
val as a Runtime Constant
#
val declares a read-only reference: its value is determined once at runtime and can’t be reassigned afterwards. It’s not a constant in the compile-time sense — its value is only known when the program runs.
val pi = 3.14159
val currentYear = Calendar.getInstance().get(Calendar.YEAR)
val hostName = InetAddress.getLocalHost().hostName
val startTime = System.currentTimeMillis()
The last three examples show the characteristic that distinguishes val from const val: its value is computed at runtime, can differ on every program execution, and can call functions or create objects.
// val whose value is computed when the object is created
class AppConfig {
val version = BuildConfig.VERSION_NAME // read from build config
val environment = System.getenv("APP_ENV") ?: "development"
val maxConnections = Runtime.getRuntime().availableProcessors() * 2
}
In this scenario, val is the right choice because the values aren’t known at compile time — they depend on the environment and hardware where the program runs.
const val — Compile-Time Constant
#
const val is a true constant. Its value must be known by the compiler at compile time, not at runtime. This means the compiler can directly “inline” the value into every place that uses it, producing more efficient bytecode.
const val API_VERSION = "v2"
const val BASE_URL = "https://api.example.com/"
const val MAX_LOGIN_ATTEMPTS = 5
const val TIMEOUT_MS = 30_000L
const val DATABASE_NAME = "myapp_db"
const val DEBUG = false
The difference in how the compiler treats val vs const val can be seen from the generated bytecode:
// Kotlin code
const val MAX = 100
val limit = 100
fun check(value: Int) = value <= MAX
fun check2(value: Int) = value <= limit
After compilation, check is equivalent to value <= 100 (the value is inlined), while check2 must fetch the limit value from memory every time it’s called. For constants accessed frequently in loops or performance-critical code, this difference is real.
const val Rules
#
const val has stricter rules than val:
const val ONLY WORKS for:
✓ Primitive types: Int, Long, Double, Float, Short, Byte, Char, Boolean
✓ String
✓ Literal values — numbers, strings, or expressions of literals
✓ Declared at file top-level or inside an object / companion object
const val CANNOT be used for:
✗ Custom reference types (classes, data classes, lists, etc.)
✗ Results of function calls (including constructors)
✗ Values only known at runtime
✗ Declared inside an ordinary class or function
// ✓ CORRECT
const val NAME = "MyApp"
const val VERSION = 2
const val PI = 3.14159
// ✗ CANNOT: function results
const val TIME = System.currentTimeMillis() // error
// ✗ CANNOT: custom types
const val CONFIG = Config() // error
// ✗ CANNOT: inside a function
fun setup() {
const val TIMEOUT = 5000 // error
}
Where to Declare Constants #
Kotlin provides three main locations for placing const val, each with different trade-offs.
Top-Level — Easiest to Access #
Declared directly at file level, outside classes or functions. This is the simplest and most direct way.
// File: Constants.kt
package com.myapp.core
const val APP_VERSION = "1.0.0"
const val API_URL = "https://api.myapp.com/v2"
const val UPLOAD_LIMIT_BYTES = 10 * 1024 * 1024 // 10 MB
const val DATE_FORMAT = "yyyy-MM-dd"
const val TIME_FORMAT = "HH:mm:ss"
How to use it in another file:
import com.myapp.core.API_URL
import com.myapp.core.DATE_FORMAT
// or import everything at once
import com.myapp.core.*
fun buildRequest(): HttpRequest {
return HttpRequest.Builder()
.url(API_URL + "/users")
.build()
}
object — Grouped Constants
#
Grouping related constants into an object makes code more organized and gives a clear namespace.
object NetworkConstants {
const val CONNECT_TIMEOUT_MS = 10_000L
const val READ_TIMEOUT_MS = 30_000L
const val MAX_RETRIES = 3
const val RETRY_BACKOFF_MS = 2_000L
const val CONNECTION_POOL_SIZE = 20
}
object ValidationConstants {
const val PASSWORD_MIN_LENGTH = 8
const val PASSWORD_MAX_LENGTH = 128
const val NAME_MIN_LENGTH = 2
const val NAME_MAX_LENGTH = 100
const val MAX_LOGIN_ATTEMPTS = 5
const val ACCOUNT_LOCK_DURATION_MIN = 30
}
Usage:
fun configureOkHttp(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(NetworkConstants.CONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.readTimeout(NetworkConstants.READ_TIMEOUT_MS, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.build()
}
companion object — Class-Bound Constants
#
When constants are tightly coupled to a class, place them inside that class’s companion object.
class HttpClient private constructor(val baseUrl: String) {
companion object {
const val AUTH_HEADER = "Authorization"
const val CONTENT_TYPE_HEADER = "Content-Type"
const val CONTENT_TYPE_JSON = "application/json"
const val CONTENT_TYPE_FORM = "application/x-www-form-urlencoded"
fun createWithUrl(url: String): HttpClient = HttpClient(url)
}
fun addJsonHeader(request: Request.Builder): Request.Builder {
return request
.header(CONTENT_TYPE_HEADER, CONTENT_TYPE_JSON)
}
}
// Access from outside
val contentType = HttpClient.CONTENT_TYPE_JSON
Diagram: Choosing the Constant Type #
flowchart TD
A{Value known\nat compile time?} -- Yes --> B{Primitive type\nor String?}
A -- No --> C["val\nruntime value"]
B -- Yes --> D{Tightly coupled\nto one class?}
B -- No --> C
D -- Yes --> E["const val\nin companion object"]
D -- No --> F{Needs\ngrouping?}
F -- Yes --> G["const val\nin object"]
F -- No --> H["const val\ntop-level"]const val in Annotations
#
One important use case where const val can’t be replaced by a regular val is as an annotation argument. JVM annotations only accept compile-time values — not runtime values.
const val TABLE_NAME = "pengguna"
const val COLUMN_ID = "id"
const val COLUMN_NAME = "nama"
const val COLUMN_EMAIL = "email"
@Entity(tableName = TABLE_NAME)
data class User(
@PrimaryKey
@ColumnInfo(name = COLUMN_ID)
val id: Long = 0,
@ColumnInfo(name = COLUMN_NAME)
val name: String,
@ColumnInfo(name = COLUMN_EMAIL)
val email: String
)
Without const val, you’re forced to write string literals directly in annotations — which is typo-prone and hard to refactor:
// ANTI-PATTERN: magic strings in annotations — typo-prone, hard to refactor
@Entity(tableName = "pengguna")
data class User(
@PrimaryKey
@ColumnInfo(name = "id")
val id: Long = 0,
@ColumnInfo(name = "nama") // if you typo "naama" — the error only surfaces at runtime
val name: String
)
// CORRECT: use const val — safe refactoring, typos caught at compile time
@Entity(tableName = TABLE_NAME)
data class User(
@PrimaryKey
@ColumnInfo(name = COLUMN_ID)
val id: Long = 0,
@ColumnInfo(name = COLUMN_NAME)
val name: String
)
val vs const val — Full Comparison
#
| Aspect | val | const val |
|---|---|---|
| Value determination time | Runtime | Compile-time |
| Supported types | All types | Primitives and String |
| Can call functions | ✓ Yes | ✗ No |
| Can create objects | ✓ Yes | ✗ No |
| Declaration location | Anywhere | Top-level, object, companion object |
| Inlined by the compiler | ✗ No | ✓ Yes |
| Usable in annotations | ✗ No | ✓ Yes |
| Access performance | Memory access | Inlined directly |
Enums as Structured Constants #
For a set of mutually exclusive values with semantic relationships, enum class is a more expressive choice than a series of const val.
// ANTI-PATTERN: const val for related values
const val STATUS_PENDING = "PENDING"
const val STATUS_PROCESSING = "PROCESSING"
const val STATUS_COMPLETED = "COMPLETED"
const val STATUS_FAILED = "FAILED"
// Nothing prevents code like this:
fun processTransaction(status: String) { ... }
processTransaction("TYP0") // ✗ not caught at compile time!
// CORRECT: enum provides type safety
enum class TransactionStatus {
PENDING, PROCESSING, COMPLETED, FAILED
}
fun processTransaction(status: TransactionStatus) { ... }
processTransaction(TransactionStatus.COMPLETED) // ✓
// processTransaction("COMPLETED") // ✗ compilation error!
Enums can be enriched with properties and functions:
enum class HttpCode(val code: Int, val message: String) {
OK(200, "Success"),
CREATED(201, "Resource created successfully"),
BAD_REQUEST(400, "Invalid request"),
UNAUTHORIZED(401, "Authentication required"),
FORBIDDEN(403, "Access denied"),
NOT_FOUND(404, "Resource not found"),
SERVER_ERROR(500, "Internal server error");
val isSuccess: Boolean get() = code in 200..299
val isClientError: Boolean get() = code in 400..499
val isServerError: Boolean get() = code in 500..599
override fun toString() = "$code $message"
}
// Usage
val response = HttpCode.NOT_FOUND
println(response) // 404 Resource not found
println(response.isSuccess) // false
println(response.isClientError) // true
// when with enums — automatic exhaustive check
fun handleResponse(code: HttpCode): String {
return when (code) {
HttpCode.OK, HttpCode.CREATED -> "Success: ${code.message}"
HttpCode.BAD_REQUEST -> "Fix your input"
HttpCode.UNAUTHORIZED -> "Please log in first"
HttpCode.FORBIDDEN -> "You don't have access to this resource"
HttpCode.NOT_FOUND -> "The resource you're looking for doesn't exist"
HttpCode.SERVER_ERROR -> "There's a problem on the server, try again later"
}
// The compiler forces all cases to be handled — you can't miss a single one
}
Managing Constants in Large Projects #
As projects grow, constant organization becomes important. A few commonly used patterns:
One File Per Domain #
src/main/kotlin/com/myapp/
├── core/
│ ├── AppConstants.kt // version, name, environment
│ ├── NetworkConstants.kt // timeout, retry, pool size
│ └── ValidationConstants.kt // field lengths, formats, regexes
├── feature/
│ ├── auth/
│ │ └── AuthConstants.kt // JWT, session, token
│ └── payment/
│ └── PaymentConstants.kt // midtrans, xendit config
Nested Object Hierarchy #
object Constants {
object Network {
const val TIMEOUT_MS = 30_000L
const val MAX_RETRIES = 3
object Header {
const val AUTHORIZATION = "Authorization"
const val CONTENT_TYPE = "Content-Type"
}
}
object Database {
const val NAME = "myapp.db"
const val VERSION = 5
const val MAX_CONNECTIONS = 10
}
object Pagination {
const val DEFAULT_PAGE_SIZE = 20
const val MAX_PAGE_SIZE = 100
}
}
// Usage
val timeout = Constants.Network.TIMEOUT_MS
val authHeader = Constants.Network.Header.AUTHORIZATION
val dbName = Constants.Database.NAME
Avoiding Magic Numbers and Magic Strings #
A magic number is a literal number that appears in code without contextual explanation — the reader has to guess its meaning.
// ANTI-PATTERN: magic numbers and magic strings
fun validatePassword(password: String): Boolean {
return password.length >= 8 &&
password.length <= 128 &&
password.matches(Regex(".*[A-Z].*")) &&
password.matches(Regex(".*[0-9].*"))
}
fun calculateDiscount(price: Double, memberCode: String): Double {
return when (memberCode) {
"GOLD" -> price * 0.80 // 20% discount — but why 0.80?
"SILVER" -> price * 0.90 // 10% discount
"BRONZE" -> price * 0.95 // 5% discount
else -> price
}
}
// CORRECT: give a name to every meaningful value
object ValidationConstants {
const val PASSWORD_MIN_LENGTH = 8
const val PASSWORD_MAX_LENGTH = 128
const val UPPERCASE_REGEX = ".*[A-Z].*"
const val DIGIT_REGEX = ".*[0-9].*"
}
object DiscountConstants {
const val GOLD_PERCENT = 20
const val SILVER_PERCENT = 10
const val BRONZE_PERCENT = 5
}
fun validatePassword(password: String): Boolean {
return password.length >= ValidationConstants.PASSWORD_MIN_LENGTH &&
password.length <= ValidationConstants.PASSWORD_MAX_LENGTH &&
password.matches(Regex(ValidationConstants.UPPERCASE_REGEX)) &&
password.matches(Regex(ValidationConstants.DIGIT_REGEX))
}
fun calculateDiscount(price: Double, memberCode: String): Double {
val discountPercent = when (memberCode) {
"GOLD" -> DiscountConstants.GOLD_PERCENT
"SILVER" -> DiscountConstants.SILVER_PERCENT
"BRONZE" -> DiscountConstants.BRONZE_PERCENT
else -> 0
}
return price * (1 - discountPercent / 100.0)
}
The rule of thumb: if a number or string appears more than once in the codebase, or its meaning isn’t immediately clear from context, make it a constant. Even once, if the value could change in the future or has a specific business meaning, it’s better to make it a constant.
Constant Naming Conventions #
Kotlin follows different conventions for regular val and const val:
// Regular val — camelCase (same as variables)
val startTime = System.currentTimeMillis()
val dbConnection = Database.create()
// const val — SCREAMING_SNAKE_CASE
const val API_URL = "https://api.example.com"
const val MAX_ATTEMPTS = 3
const val TIMEOUT_MS = 30_000L
// Enum — PascalCase for the enum name, SCREAMING_SNAKE_CASE for entries
// or camelCase for entries — both are common, pick one and be consistent
enum class OrderStatus { PENDING, PROCESSING, SHIPPED, COMPLETED }
SCREAMING_SNAKE_CASE for const val is a convention inherited from Java and followed by the entire Kotlin ecosystem. It makes constants easy to recognize at a glance among regular variables.
Summary #
valfor runtime constants — its value can be computed while the program runs, can call functions, can create objects. Suitable for configuration that depends on the environment or hardware.const valfor compile-time constants — the value must be certain at compile time, only for primitive types and String. The compiler inlines the value for better performance.const valis the only option for annotations — annotation arguments must be compile-time constants; a regularvalcan’t be used here.- Three declaration locations — top-level for general constants,
objectfor grouped per-domain constants,companion objectfor constants tightly bound to one class.- Enums for related values — more type-safe than a series of
const val. The compiler forces all cases to be handled inwhen, and nothing outside the enum can be passed in.- Avoid magic numbers and magic strings — every number or string with business meaning must have a name. This makes refactoring safe and code more readable.
- Naming conventions —
valfollows camelCase like regular variables;const valuses SCREAMING_SNAKE_CASE for easy recognition at a glance.- Organization in large projects — group constants per domain into separate files or objects. Avoid one giant
Constants.ktfile containing every constant from the whole application.