@Value: Opini Engineering tentang Konfigurasi di Spring Boot
Konfigurasi adalah salah satu bagian kode yang paling sering diremehkan. Selama aplikasi bisa jalan, urusan selesai. Akibatnya @Value tersebar di mana-mana — di service, di controller, bahkan di utility class — dan tidak ada yang mempermasalahkannya sampai enam bulan kemudian ketika tim harus me-rename satu property dan mendapati dirinya harus grep seluruh codebase. Masalahnya bukan karena @Value itu buruk. Masalahnya adalah @Value terlalu mudah, sehingga sering dipakai tanpa sadar konsekuensi jangka panjangnya. Artikel ini membahas konfigurasi bukan sekadar dari sisi bagaimana memakainya, tapi dari sisi mengapa pilihan yang salah bisa berubah menjadi technical debt tersembunyi yang mahal untuk dibayar.
Konfigurasi adalah Kontrak, Bukan Sekadar Nilai
Sebelum membahas kode, ada pergeseran cara pandang yang perlu terjadi. Konfigurasi bukan variabel biasa yang kebetulan disimpan di luar binary. Secara engineering, konfigurasi adalah:
Konfigurasi = Kontrak antara aplikasi dan environment-nya
Ini berarti konfigurasi memiliki konsekuensi yang sama seperti public API: perubahan harus sadar, terstruktur, dan terdokumentasi. Jika konfigurasi diperlakukan seperti string acak yang dilempar ke field tanpa struktur, desain aplikasi ikut terdegradasi. Bug dari misconfig tidak langsung terlihat — ia baru meledak saat runtime, seringkali di production.
flowchart LR
subgraph "Konfigurasi sebagai String Acak"
A1["@Value scattered\ndi 12 class berbeda"] --> B1["Satu property rename\n= grep seluruh codebase"]
B1 --> C1["Salah config\n= crash saat runtime"]
end
subgraph "Konfigurasi sebagai Kontrak"
A2["Satu class/record\nper domain config"] --> B2["Perubahan terlokalisasi\ndi satu tempat"]
B2 --> C2["Validasi saat startup\nbukan saat runtime"]
end@Value — Nyaman di Awal, Mahal di Akhir
@Value adalah cara termudah untuk mengambil nilai dari application.properties atau application.yaml. Spring menginject nilai langsung ke field menggunakan SpEL (Spring Expression Language).
// Terlihat rapi — tapi ini awal dari masalah
@Service
public class JwtService {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expired}")
private long expired;
}
Masalah tidak muncul hari pertama. Masalah muncul enam bulan kemudian:
// ANTI-PATTERN: @Value tersebar di banyak class
@Service
public class JwtService {
@Value("${jwt.secret}")
private String jwtSecret; // duplikat di TokenValidator.java
}
@Component
public class TokenValidator {
@Value("${jwt.secret}") // duplikat — tidak ada single source of truth
private String secret;
}
@Component
public class RefreshTokenService {
@Value("${jwt.expired}") // property ini sudah rename jadi jwt.access.expired
private long expired; // akan crash saat startup — tapi hanya di sini
}
Empat Masalah Struktural @Value
Configuration scattering. Tidak ada satu tempat yang menggambarkan keseluruhan struktur konfigurasi. Developer baru harus grep seluruh codebase untuk menemukan property mana saja yang digunakan.
Tidak ada kontrak tipe. @Value selalu berupa String sampai runtime. Tidak ada yang mencegah kamu menulis @Value("${jwt.expired}") ke field bertipe String padahal yang tersimpan adalah angka — error baru muncul saat field tersebut pertama kali digunakan.
Refactor berisiko tinggi. Rename satu property di YAML berarti harus mencari dan mengganti semua @Value("${nama.property}") yang tersebar. Ada yang terlewat? Crash saat runtime, bukan saat compile.
Validasi nol besar. Jika property tidak ada di file konfigurasi dan tidak ada default value, Spring akan gagal startup — tapi tanpa pesan error yang jelas tentang apa yang kurang.
Kapan @Value Masih Bisa Diterima
Gunakan @Value jika:
✓ Nilai tunggal yang berdiri sendiri (feature flag boolean)
✓ PoC, prototype, atau tooling internal
✓ Property yang benar-benar hanya dipakai di satu tempat
Hindari @Value jika:
✗ Konfigurasi punya lebih dari 2–3 property yang saling terkait
✗ Property dipakai di lebih dari satu class
✗ Aplikasi akan dikelola tim dan tumbuh dalam jangka panjang
✗ Nilai memerlukan validasi (format, range, keberadaan)
@ConfigurationProperties — Konfigurasi sebagai Domain
Jika @Value adalah shortcut, @ConfigurationProperties adalah keputusan desain. Dengan mendefinisikan sebuah class konfigurasi, kamu menyatakan secara eksplisit: “Konfigurasi ini adalah satu domain yang utuh, dan saya bertanggung jawab atas strukturnya.”
# application.yaml
jwt:
secret: my-super-secret-key-minimum-256-bits
access-token:
expired: 15m
refresh-token:
expired: 7d
// BENAR: satu class untuk satu domain konfigurasi
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtProperties {
private String secret;
private Duration accessTokenExpired;
private Duration refreshTokenExpired;
// getters dan setters...
}
Perubahan yang langsung terasa:
// ANTI-PATTERN: inject @Value di setiap class yang butuh JWT config
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.access-token.expired}")
private Duration accessTokenExpired;
}
// BENAR: inject satu properties object, semua tersedia
@Service
public class JwtService {
private final JwtProperties jwtProperties;
public JwtService(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
}
public String generateToken(String subject) {
return Jwts.builder()
.setSubject(subject)
.setExpiration(Date.from(
Instant.now().plus(jwtProperties.getAccessTokenExpired())
))
.signWith(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes()))
.compact();
}
}
Validasi dengan @Validated
Salah satu keunggulan terbesar @ConfigurationProperties adalah kemampuannya dikombinasikan dengan Bean Validation untuk memvalidasi konfigurasi saat startup — bukan saat runtime:
@ConfigurationProperties(prefix = "jwt")
@Component
@Validated
public class JwtProperties {
@NotBlank(message = "JWT secret tidak boleh kosong")
@Size(min = 32, message = "JWT secret minimal 32 karakter untuk keamanan")
private String secret;
@NotNull(message = "Access token expiry wajib dikonfigurasi")
@DurationMin(minutes = 5, message = "Access token minimal 5 menit")
private Duration accessTokenExpired;
@NotNull(message = "Refresh token expiry wajib dikonfigurasi")
private Duration refreshTokenExpired;
// getters dan setters...
}
Dengan @Validated, aplikasi menolak startup jika konfigurasi tidak valid. Ini jauh lebih baik daripada membiarkan aplikasi berjalan dengan konfigurasi yang salah dan crash di tengah proses bisnis.
Fail-fast adalah prinsip penting dalam desain sistem yang tangguh. Validasi konfigurasi saat startup — bukan saat pertama kali digunakan — adalah salah satu implementasi paling konkret dari prinsip ini. Error konfigurasi yang terdeteksi saat startup jauh lebih mudah di-debug daripada yang baru muncul di production setelah traffic masuk.
record — Immutable Configuration
@ConfigurationProperties dengan class biasa masih menyisakan satu kelemahan: konfigurasi bersifat mutable. Ada setter, ada kemungkinan state berubah, ada kemungkinan disalahgunakan sebagai tempat menyimpan state runtime. Java record menutup celah ini.
Konfigurasi seharusnya dibaca saat startup dan tidak berubah seumur hidup aplikasi. record memaksa disiplin ini secara struktural — tidak ada setter, tidak ada mutasi, tidak ada ambiguitas.
// ANTI-PATTERN: class mutable yang bisa diubah setelah diinject
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtProperties {
private String secret;
private Duration accessTokenExpired;
// setter ada — tidak ada yang mencegah ini dipanggil dari mana saja
public void setSecret(String secret) { this.secret = secret; }
public void setAccessTokenExpired(Duration d) { this.accessTokenExpired = d; }
}
// BENAR: record — immutable by design
@ConfigurationProperties(prefix = "jwt")
public record JwtProperties(
String secret,
Duration accessTokenExpired,
Duration refreshTokenExpired
) {}
Dengan record, konfigurasi menjadi self-documented: siapapun yang membaca deklarasinya langsung tahu semua field yang tersedia, tipe datanya, dan tidak perlu menelusuri setter untuk memahami strukturnya.
// Penggunaan record lebih bersih — akses via accessor method, bukan getter
@Service
public class JwtService {
private final JwtProperties jwt;
public JwtService(JwtProperties jwt) {
this.jwt = jwt;
}
public String generateAccessToken(String subject) {
return Jwts.builder()
.setSubject(subject)
.setExpiration(Date.from(Instant.now().plus(jwt.accessTokenExpired())))
.signWith(Keys.hmacShaKeyFor(jwt.secret().getBytes()))
.compact();
}
}
Validasi pada record
record tetap bisa dikombinasikan dengan @Validated:
@ConfigurationProperties(prefix = "jwt")
@Validated
public record JwtProperties(
@NotBlank @Size(min = 32)
String secret,
@NotNull
Duration accessTokenExpired,
@NotNull
Duration refreshTokenExpired
) {}
Untuk menggunakanrecorddengan@ConfigurationProperties, pastikan Spring Boot versi 2.6 ke atas dan tidak ada@Componentdirecord— cukup daftarkan via@EnableConfigurationProperties(JwtProperties.class)di configuration class, atau via@ConfigurationPropertiesScandi main class.
Konversi Nama Otomatis (Relaxed Binding)
Spring Boot melakukan normalisasi nama property secara otomatis — ini disebut relaxed binding. Semua format berikut akan di-bind ke field Java yang sama:
| Format di YAML / Properties | Field Java |
|---|---|
access-token (kebab-case) | accessToken |
access_token (underscore) | accessToken |
accessToken (camelCase) | accessToken |
ACCESSTOKEN (uppercase) | accessToken |
ACCESS_TOKEN (screaming snake) | accessToken |
Ini berarti kamu bisa menulis YAML dengan kebab-case (yang merupakan konvensi yang direkomendasikan Spring) dan Java dengan camelCase tanpa perlu konfigurasi tambahan:
# Direkomendasikan: kebab-case di YAML
app:
max-connection-pool-size: 20
default-page-size: 50
cache-ttl-seconds: 300
// camelCase di Java — Spring menghubungkan keduanya secara otomatis
@ConfigurationProperties(prefix = "app")
public record AppProperties(
int maxConnectionPoolSize, // ← dari max-connection-pool-size
int defaultPageSize, // ← dari default-page-size
int cacheTtlSeconds // ← dari cache-ttl-seconds
) {}
Mengelola Konfigurasi Multi-Environment
Proyek nyata memiliki lebih dari satu environment. Spring Boot menangani ini dengan profile-specific configuration files:
src/main/resources/
├── application.yaml ← default, semua environment
├── application-dev.yaml ← override untuk development
├── application-staging.yaml ← override untuk staging
└── application-prod.yaml ← override untuk production
# application.yaml — nilai default yang berlaku untuk semua environment
app:
jwt:
secret: dev-secret-change-in-prod
access-token:
expired: 1h
database:
pool-size: 5
cache:
enabled: true
ttl: 300s
# application-prod.yaml — hanya override yang berbeda
app:
jwt:
access-token:
expired: 15m # lebih ketat di production
database:
pool-size: 20 # lebih besar di production
flowchart TD
A["application.yaml\n(base config)"] --> D["Merged Config"]
B["application-{profile}.yaml\n(profile override)"] --> D
C["Environment Variables\n(highest priority)"] --> D
D --> E["@ConfigurationProperties\nbound & validated"]
E --> F["Application Ready"]Jangan pernah menyimpan secret (password database, API key, JWT secret) di dalam file konfigurasi yang masuk ke version control. Gunakan environment variable, secret manager (AWS Secrets Manager, HashiCorp Vault), atau Kubernetes Secrets untuk nilai sensitif. File application-prod.yaml di repository hanya boleh berisi konfigurasi non-sensitif.Perbandingan Tiga Pendekatan
flowchart TD
A{Berapa property\nyang terkait?} -- "1–2, berdiri sendiri" --> B{"Akan tumbuh\nke depan?"}
B -- Tidak --> C["@Value\n✓ Sederhana\n✗ Tidak skalabel"]
B -- Ya --> D["@ConfigurationProperties\n✓ Terstruktur\n✓ Bisa divalidasi"]
A -- "3+, satu domain" --> D
D --> E{"Tim besar atau\nsistem kritis?"}
E -- Ya --> F["@ConfigurationProperties\n+ record\n✓ Immutable\n✓ Self-documented"]
E -- Tidak --> D| Kriteria | @Value | @ConfigurationProperties | + record |
|---|---|---|---|
| Kecepatan awal | ✓ Cepat | ✗ Butuh class | ✗ Butuh class |
| Single source of truth | ✗ Tersebar | ✓ Terpusat | ✓ Terpusat |
| Type safety | ✗ Semua string | ✓ Tipe eksplisit | ✓ Tipe eksplisit |
| Validasi startup | ✗ Tidak ada | ✓ Dengan @Validated | ✓ Dengan @Validated |
| Immutability | ✗ Mutable | ✗ Mutable | ✓ Immutable |
| Refactor safety | ✗ Rawan miss | ✓ IDE-friendly | ✓ IDE-friendly |
| Ramah tim besar | ✗ | ✓ | ✓ |
| Cocok untuk microservice | ✗ | ⚠ | ✓ |
Rekomendasi Berdasarkan Konteks
Startup / PoC / tooling internal:
→ @Value boleh digunakan, tapi sadar bahwa ini akan menjadi debt jika diteruskan
Produk yang akan tumbuh:
→ Langsung @ConfigurationProperties dari hari pertama
→ Tambahkan @Validated untuk semua property yang kritis
Microservice / sistem dengan tim lebih dari 3 orang:
→ @ConfigurationProperties + record sebagai standar
→ Semua secret via environment variable atau secret manager
Sistem regulated (fintech, healthcare):
→ @ConfigurationProperties + record + @Validated adalah minimum
→ Audit trail perubahan konfigurasi wajib ada
Checklist Konfigurasi Spring Boot
STRUKTUR:
□ Satu class/record per domain konfigurasi (jwt, database, cache, ...)
□ Tidak ada @Value untuk property yang terkait dengan domain yang sudah punya Properties class
□ Nama YAML menggunakan kebab-case, nama Java menggunakan camelCase
VALIDASI:
□ @Validated digunakan pada semua @ConfigurationProperties yang kritis
□ Constraint annotation (@NotBlank, @NotNull, @Min, dll.) ada di semua field wajib
□ Aplikasi ditested startup dengan konfigurasi yang sengaja dikosongkan
KEAMANAN:
□ Tidak ada secret di file konfigurasi yang masuk ke version control
□ Nilai sensitif menggunakan environment variable atau secret manager
□ application-prod.yaml tidak berisi password, key, atau token
ARSITEKTUR:
□ @ConfigurationProperties diinject via constructor, bukan field injection
□ Properties object tidak digunakan sebagai state — hanya dibaca
□ Pertimbangkan record untuk semua Properties class baru
Ringkasan
- Konfigurasi adalah kontrak antara aplikasi dan environment — perlakukan seperti public API, bukan string biasa.
@Valueterlalu mudah — kenyamanannya di awal berbanding terbalik dengan biaya maintenance-nya di masa depan; gunakan hanya untuk nilai tunggal yang benar-benar berdiri sendiri.@ConfigurationPropertiesmengubah konfigurasi tersebar menjadi satu domain terpusat dengan tipe eksplisit dan kemampuan validasi saat startup.@Validatedpada@ConfigurationPropertiesadalah implementasi fail-fast yang paling bernilai — aplikasi menolak startup jika konfigurasi tidak valid, bukan crash di tengah proses.recordmembawa konfigurasi ke level immutable — tidak ada setter, tidak ada mutasi, tidak ada ambiguitas; inilah standar untuk sistem yang serius.- Relaxed binding Spring Boot memungkinkan kebab-case di YAML dan camelCase di Java tanpa konfigurasi tambahan — gunakan konvensi ini secara konsisten.
- Secret tidak boleh ada di version control — gunakan environment variable, secret manager, atau Kubernetes Secrets untuk semua nilai sensitif.
- Pilih pendekatan berdasarkan konteks:
@Valueuntuk PoC,@ConfigurationPropertiesuntuk produk yang tumbuh,recorduntuk sistem kritis dan tim besar.