Feature flag adalah saklar di dalam kode yang menentukan apakah sebuah fitur aktif, dan untuk siapa, tanpa deploy ulang. Bayangin instalasi listrik rumah baru. Tukang sudah memasang kabel dan lampu di semua kamar (itu deploy), tapi lampunya baru menyala saat saklarnya kamu tekan (itu release). Kamu bisa menyalakan satu kamar dulu untuk dicoba. Dan kalau ada korsleting, kamu turunkan MCB-nya: listrik mati seketika tanpa membongkar kabel (itu kill switch).
🧪 Tebakan cepat: fitur ekspor PDF di catatan-api dinyalakan untuk 25% user, dan sari kebagian. Besoknya kamu naikkan jadi 50%. Sari masih dapat fiturnya, atau diundi ulang dan bisa saja kehilangan fitur itu? Tebak dulu, jawabannya ada di bagian "Bucket: nomor tetap untuk tiap user".
Deploy bukan release
Di pelajaran CI/CD dan Deployment Strategies, kode baru catatan-api sampai ke web-1 lewat pipeline. Itu deploy: kodenya sudah ada di server. Release adalah saat user benar-benar bisa memakai fiturnya. Feature flag memisahkan dua momen itu.
Tim catatan-api bikin fitur baru: ekspor catatan jadi PDF. Kodenya di-deploy hari Senin, tapi disembunyikan di balik flag bernama ekspor-pdf:
app.get("/catatan/:id/pdf", (req, res) => {
if (!evaluateFlag(flags["ekspor-pdf"], req.user)) {
return res.status(404).json({ error: "Fitur belum tersedia" });
}
// ... bikin PDF dari catatan
});
- Nilai
flagsdisimpan di luar kode: tabel database, file config yang dibaca ulang, atau layanan flag seperti LaunchDarkly, Unleash, PostHog, dan Flagsmith. Jadi mengubah flag gak butuh deploy. - Selama flag-nya mati, endpoint itu menjawab seolah fiturnya belum ada.
- Kalau fiturnya bermasalah, matikan flag-nya: dalam hitungan detik semua user kembali ke perilaku lama. Rollback deploy butuh beberapa menit, karena pipeline harus jalan lagi.
Empat aturan, dicek berurutan
Satu flag di catatan-api bentuknya begini:
const flag = {
enabled: true, // saklar utama, alias kill switch
percentage: 25, // berapa persen user yang kebagian, 0 sampai 100
allowlist: ["dewi"], // selalu dapat: dewi tim internal yang menguji
blocklist: ["bayu"], // gak pernah dapat: PDF bayu sempat crash
};
Evaluator mengecek aturannya dari atas ke bawah, dan aturan pertama yang cocok langsung menentukan jawabannya:
enabledfalse? Jawabannyafalseuntuk semua orang, termasuk dewi. Ini kill switch.user.idada diblocklist?false.user.idada diallowlist?true.- Persentase:
100atau lebih berartitrue,0atau kurang berartifalse, selain itu bandingkan bucket user dengan persentase.
| User | Bucket | Aturan yang memutuskan | Hasil |
|---|---|---|---|
| dewi | 55 | 3. allowlist | true |
| bayu | 3 | 2. blocklist (walau 3 < 25) | false |
| sari | 21 | 4. 21 < 25 | true |
| budi | 60 | 4. 60 tidak < 25 | false |
Urutan ini penting. Kalau allowlist dicek sebelum enabled, kill switch gak bisa mematikan fitur untuk dewi.
Bucket: nomor tetap untuk tiap user
Kenapa gak pakai Math.random() * 100 < 25 saja? Karena undiannya diulang di tiap request: sari melihat tombol PDF, refresh, tombolnya hilang, refresh lagi, muncul. Yang dibutuhkan adalah angka yang selalu sama untuk user yang sama. Caranya: hitung hash dari user.id, yaitu angka yang dihitung dari teks, dan teks yang sama selalu menghasilkan angka yang sama. Angka 0 sampai 99 itu disebut bucket.
Rumusnya: mulai dari hash = 0, lalu untuk tiap karakter, hash = (hash * 31 + kode karakter) % 100. Kode karakter diambil dengan charCodeAt(0). Untuk "sari":
| Karakter | Kode | Hitungan | Hash |
|---|---|---|---|
| s | 115 | (0 × 31 + 115) % 100 | 15 |
| a | 97 | (15 × 31 + 97) % 100 = 562 % 100 | 62 |
| r | 114 | (62 × 31 + 114) % 100 = 2036 % 100 | 36 |
| i | 105 | (36 × 31 + 105) % 100 = 1221 % 100 | 21 |
Bucket sari 21. Dengan 25%, 21 < 25, jadi sari dapat. Naik ke 50%, 21 < 50, jadi sari tetap dapat. Bucket-nya gak berubah, yang berubah cuma batasnya. Itu jawaban tebakannya. Menaikkan persentase cuma menambah user baru (yang bucket-nya 25 sampai 49), gak pernah mencabut fitur dari yang sudah dapat.
Kenapa dikali 31 dan bukan dijumlah saja? Kalau dijumlah, "ab" dan "ba" dapat bucket yang sama. Dengan perkalian, urutan huruf ikut menentukan hasilnya.
Kode evaluator lengkap
function evaluateFlag(flag, user) {
if (!flag.enabled) return false; // 1. kill switch
if (flag.blocklist.includes(user.id)) return false; // 2. blocklist
if (flag.allowlist.includes(user.id)) return true; // 3. allowlist
if (flag.percentage >= 100) return true; // 4. persentase
if (flag.percentage <= 0) return false;
let hash = 0;
for (const char of user.id) {
hash = (hash * 31 + char.charCodeAt(0)) % 100;
}
return hash < flag.percentage;
}
const flag = { enabled: true, percentage: 25, allowlist: ["dewi"], blocklist: ["bayu"] };
console.log(evaluateFlag(flag, { id: "sari", plan: "free" })); // true
console.log(evaluateFlag(flag, { id: "budi", plan: "pro" })); // false
!flag.enableddicek paling awal, jadi kill switch menang dari semua aturan lain.includes(user.id)mengecek apakah id user ada di array.>= 100dan<= 0menangani dua ujung tanpa menghitung hash.for (const char of user.id)berjalan per karakter.% 100di tiap langkah menjaga angkanya tetap 0 sampai 99.return hash < flag.percentagemengembalikan boolean,trueataufalse, bukan teks.
Menaikkan rollout dengan aman
Rollout bertahap catatan-api: dewi dulu lewat allowlist, lalu 5%, 25%, 50%, 100%. Di tiap tahap, pantau error rate endpoint PDF. Dengan 20 user di Lab:
| Tahap | User yang dapat | Error PDF |
|---|---|---|
| 0% | 1 (dewi, lewat allowlist) | 0% |
| 25% | 5 (intan, tari, reza, sari, plus dewi) | 0% |
| 50% | 10 (joko ikut masuk) | 10% |
Di 50%, PDF joko crash karena lampirannya besar, sama seperti bayu dulu, dan error rate langsung 10%. Set enabled: false, dan dalam hitungan detik gak ada lagi yang memanggil kode PDF, sementara tim mencari bug-nya dengan tenang.
Layanan flag sungguhan juga punya aturan target berdasarkan atribut, misalnya "user dengan plan pro selalu dapat" atau "cuma user di Indonesia". Konsepnya sama: aturan dicek berurutan, persentase jadi jaring terakhir.
Flag debt: membersihkan flag
Setelah ekspor PDF jalan 100% selama dua minggu tanpa masalah, flag ekspor-pdf sudah gak ada gunanya. Kalau dibiarkan, kode penuh if untuk flag yang selalu true, dan kombinasi flag yang harus dites membengkak: 10 flag berarti 2 pangkat 10 = 1.024 kombinasi on/off. Ini disebut flag debt, risiko utama feature flag yang gak dikelola. Hapus if-nya dan flag-nya, sama seperti membersihkan kode mati.
💡 Jenis flag
- Release flag: menyembunyikan fitur yang sedang di-rollout, seperti
ekspor-pdf. Sementara, hapus setelah 100%. - Experiment flag: membagi user ke varian A dan B untuk membandingkan angka bisnis (A/B test). Sementara.
- Ops flag (kill switch): mematikan bagian yang berat atau rawan saat insiden, misalnya pratinjau lampiran. Boleh permanen.
- Permission flag: fitur khusus paket tertentu, misalnya user pro. Permanen.
Beri flag sementara tanggal kedaluwarsa dan tiket pembersihan sejak hari dibuat.
⚠️ Jebakan yang sering ketemu
- Pakai
Math.random()di tiap request. Fiturnya muncul dan hilang bergantian untuk user yang sama. Hitung bucket dari hashuser.id. - Urutan aturan terbalik. Allowlist sebelum
enabledbikin kill switch gak mematikan semua orang. Blocklist sesudah persentase bikin bayu tetap kebagian karena bucket-nya 3. - Lupa dua ujung persentase.
percentage: 0harusfalseuntuk semua (kecuali allowlist),100harustrueuntuk semua (kecuali blocklist). - Flag dibiarkan setelah 100%. Itu flag debt. Jadwalkan pembersihannya.
TL;DR: Feature flag memisahkan deploy (kode sampai di server) dari release (user bisa memakai fitur), jadi fitur bisa dinyalakan bertahap dan dimatikan dalam hitungan detik tanpa rollback. Evaluator mengecek berurutan: enabled false berarti false untuk semua (kill switch), lalu blocklist (false), allowlist (true), lalu persentase: >= 100 true, <= 0 false, selain itu bucket < percentage. Bucket dihitung dari hash user.id (hash = (hash * 31 + kode karakter) % 100), jadi user yang sama selalu dapat jawaban yang sama, dan menaikkan persentase gak mencabut fitur dari yang sudah dapat. Hapus flag sementara setelah 100% supaya gak jadi flag debt.