@Value: Opini Engineering tentang Konfigurasi di Spring Boot
9 min read

@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 menggunakan record dengan @ConfigurationProperties, pastikan Spring Boot versi 2.6 ke atas dan tidak ada @Component di record — cukup daftarkan via @EnableConfigurationProperties(JwtProperties.class) di configuration class, atau via @ConfigurationPropertiesScan di 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 / PropertiesField 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.
  • @Value terlalu mudah — kenyamanannya di awal berbanding terbalik dengan biaya maintenance-nya di masa depan; gunakan hanya untuk nilai tunggal yang benar-benar berdiri sendiri.
  • @ConfigurationProperties mengubah konfigurasi tersebar menjadi satu domain terpusat dengan tipe eksplisit dan kemampuan validasi saat startup.
  • @Validated pada @ConfigurationProperties adalah implementasi fail-fast yang paling bernilai — aplikasi menolak startup jika konfigurasi tidak valid, bukan crash di tengah proses.
  • record membawa 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: @Value untuk PoC, @ConfigurationProperties untuk produk yang tumbuh, record untuk sistem kritis dan tim besar.

Portofolio