Feature Flag di NestJS: Rollout Bertahap dan Kill Switch

Foto oleh zombieite on flickr
Mulai dengan satu service injectable yang menyimpan map aturan flag dan menyediakan method isEnabled. Awalnya cukup butuh persentase rollout dan stickiness key yang stabil seperti tenant id. Penargetan tenant dan pengecekan kill switch bisa ditambahkan belakangan tanpa mengubah cara bagian lain aplikasi memanggil service tersebut.
Tanpa identitas yang stabil di balik pembagian bucket, pengguna yang sama bisa melihat perilaku baru pada satu request dan perilaku lama pada request berikutnya, yang merusak fitur apa pun yang melibatkan lebih dari satu request. Melakukan hashing terhadap stickiness key seperti user id atau tenant id bersama key flag memastikan identitas yang sama selalu jatuh ke bucket yang sama.
Per tenant biasanya menjadi pilihan default yang lebih aman untuk produk B2B dan ERP. Jika pengguna yang berbeda di dalam perusahaan yang sama melihat perilaku yang tidak konsisten, hal itu memicu tiket dukungan yang membingungkan dan jejak audit yang tidak konsisten, meskipun setiap pengguna secara teknis sudah masuk bucket yang benar sesuai persentase rollout.
Kill switch adalah flag yang evaluasinya selalu mengalahkan semua aturan lain, termasuk persentase rollout dan penargetan tenant, sehingga bisa mematikan sebuah fitur secara instan saat insiden terjadi. Flag rollout biasa dimaksudkan untuk menyalakan fitur secara bertahap, sementara kill switch murni ada untuk mematikannya dengan cepat tanpa deployment ulang.
Berikan pemilik dan perkiraan tanggal penghapusan pada setiap flag sejak dibuat, dan jadwalkan penghapusan flag sebagai pull request kecil tersendiri begitu rollout mencapai seratus persen dan bertahan selama satu siklus rilis penuh. Meninjau daftar flag dalam sinkronisasi engineering yang berulang membuat pembersihan tidak bergantung pada ingatan seseorang.

Foto oleh zombieite on flickr
Environment variable memang cukup untuk mengaktifkan atau menonaktifkan sebuah fitur antara staging dan production, tetapi caranya tidak lagi memadai begitu kamu perlu merilis perubahan secara bertahap. Backend NestJS yang hanya mengandalkan process.env tidak bisa menjawab pertanyaan seperti apakah tenant ini seharusnya melihat alur invoicing baru, atau apakah sepuluh persen request seharusnya diarahkan ke mesin pricing baru sementara sisanya tetap memakai yang lama. Pertanyaan seperti itu butuh evaluasi saat runtime, bukan deployment ulang.
Tulisan ini membahas cara menambahkan evaluasi feature flag yang sesungguhnya ke sebuah service NestJS: sebuah evaluator kecil yang mendukung rollout persentase dengan stickiness yang stabil, allow list dan deny list per tenant, serta kill switch yang selalu menang apa pun kondisi rollout-nya. Tulisan ini juga membahas sisi operasional yang sering dilewatkan tutorial lain, yaitu cara menjaga daftar flag yang terus bertambah agar tidak berubah menjadi logika percabangan permanen yang tidak pernah dibersihkan siapa pun.
Environment variable berlaku untuk seluruh proses dan membutuhkan restart atau deployment ulang untuk diubah. Itu wajar untuk konfigurasi yang jarang berubah, seperti connection string database, tetapi menjadi alat yang salah untuk fitur yang ingin kamu aktifkan bagi lima persen traffic hari ini dan delapan puluh persen besok. Setiap langkah bertahap berubah menjadi sebuah deployment, yang justru memperlambat hal yang seharusnya dipercepat oleh rollout bertahap, yaitu iterasi yang cepat dan berisiko rendah.
Lapisan feature flag yang tepat memisahkan pertanyaan tentang kode apa yang ada di dalam artefak yang sudah di-deploy, dari pertanyaan tentang perilaku apa yang seharusnya dilihat sebuah request tertentu. Kode untuk jalur lama maupun baru sama-sama dirilis bersamaan, dan sebuah langkah evaluasi yang ringan menentukan mana yang dijalankan, per request, per tenant, atau per pengguna, berdasarkan aturan yang bisa berubah dalam hitungan detik tanpa menyentuh pipeline build.
Kamu tidak perlu vendor berbayar untuk mendapatkan perilaku rollout yang sesungguhnya. Satu injectable NestJS yang menyimpan aturan flag dan menyediakan method isEnabled sudah cukup untuk sebagian besar kebutuhan internal, dan antarmuka yang sama nantinya bisa diganti dengan provider hosted seperti Unleash atau backend yang kompatibel dengan OpenFeature tanpa mengubah kode pemanggilnya.
// flags/flag-evaluator.service.ts
import { Injectable } from '@nestjs/common';
import { createHash } from 'crypto';
interface FlagRule {
key: string;
enabled: boolean;
rolloutPercentage: number; // 0-100
tenantAllowList?: string[];
tenantDenyList?: string[];
killSwitch?: boolean;
}
@Injectable()
export class FlagEvaluatorService {
constructor(private readonly rules: Map<string, FlagRule>) {}
isEnabled(flagKey: string, stickyId: string, tenantId?: string): boolean {
const rule = this.rules.get(flagKey);
if (!rule) return false;
// Kill switch always wins, regardless of rollout state
if (rule.killSwitch) return false;
if (!rule.enabled) return false;
if (tenantId && rule.tenantDenyList?.includes(tenantId)) return false;
if (tenantId && rule.tenantAllowList?.includes(tenantId)) return true;
return this.hashBucket(flagKey, stickyId) < rule.rolloutPercentage;
}
// Deterministic 0-99 bucket so the same stickyId always
// lands in the same bucket for a given flag.
private hashBucket(flagKey: string, stickyId: string): number {
const hash = createHash('sha256')
.update(`${flagKey}:${stickyId}`)
.digest('hex');
return parseInt(hash.slice(0, 8), 16) % 100;
}
}Detail pentingnya ada pada langkah hashing. Alih-alih memanggil generator angka acak di setiap request, evaluator melakukan hashing terhadap identitas yang stabil, misalnya user id atau tenant id, digabung dengan key flag, lalu membagi hasilnya ke dalam angka antara nol sampai sembilan puluh sembilan. Bucket tersebut bersifat deterministik: pengguna yang sama selalu jatuh ke bucket yang sama untuk flag yang sama, sehingga pengguna yang masuk rollout pada satu request tetap berada di rollout pada request berikutnya, meskipun tidak ada state yang disimpan di mana pun.
Simpan hasil pencarian aturan flag di memori dan segarkan secara berkala atau lewat event invalidasi pub atau sub, alih-alih memanggil flag store di setiap request. Evaluasi itu sendiri seharusnya menjadi fungsi murni yang berjalan di dalam proses setelah aturan dimuat, sehingga biayanya hanya mikrodetik, bukan satu kali round trip jaringan.
Rollout persentase hanya berfungsi jika identitas yang sama terus mendapatkan jawaban yang sama. Tanpa stickiness key yang stabil, seorang pengguna bisa melihat alur checkout baru pada satu request dan alur lama pada request berikutnya, yang merusak fitur apa pun yang melibatkan lebih dari satu request, seperti wizard multi langkah atau keranjang belanja yang sedang berjalan.
| Stickiness key | Apa yang tetap konsisten | Paling cocok untuk |
|---|---|---|
| User id | Pengguna yang sama yang sedang login selalu mendapat bucket yang sama | Fitur tingkat akun, redesain UI, perubahan harga |
| Tenant id | Setiap pengguna di dalam satu perusahaan atau workspace berbagi satu bucket yang sama | Rollout B2B dan ERP di mana perilaku yang tidak konsisten di dalam satu perusahaan memicu tiket dukungan |
| Session id | Konsisten hanya untuk sesi saat ini, kembali berubah setelah login berikutnya | Eksperimen berumur pendek dan traffic anonim |
Untuk sebagian besar backend service, tenant id adalah stickiness key yang tepat, bukan user id. Jika separuh tim akunting di sebuah perusahaan melihat alur persetujuan baru dan separuhnya lagi tidak, kamu akan mendapatkan tiket dukungan yang membingungkan dan jejak audit yang tidak konsisten, meskipun setiap pengguna secara teknis sudah masuk bucket yang benar.
Rollout persentase menjawab seberapa besar traffic yang melihat sebuah perubahan, sementara penargetan menjawab siapa secara spesifik yang seharusnya atau tidak seharusnya melihatnya terlepas dari persentasenya. Ini paling penting dalam konteks B2B dan ERP, di mana satu pelanggan enterprise mungkin perlu menahan sebuah fitur sampai tim finance mereka menyetujuinya, sementara pelanggan pilot yang kooperatif perlu mengaktifkannya lebih awal untuk mendapatkan feedback.
Evaluasi aturan penargetan sebelum pengecekan rollout persentase, dan selalu biarkan deny list menang di atas allow list dan di atas persentase rollout. Urutan itu sesuai dengan cara tim support benar-benar bernalar saat menangani insiden, pertanyaan pertama setelah sebuah laporan bug biasanya adalah apakah tenant ini seharusnya memang mendapatkan fitur tersebut.
Hindari menyimpan daftar tenant id individual yang terus bertambah langsung di dalam setiap aturan flag. Cara itu masih baik-baik saja pada sepuluh tenant, tapi berubah menjadi kumpulan data yang tidak terbaca dan tidak bisa diaudit pada beberapa ratus tenant. Kelompokkan tenant ke dalam segmen bernama seperti pilot-customers atau enterprise-tier begitu daftarnya melebihi segelintir entri, lalu rujuk segmen tersebut dari flag alih-alih daftar mentahnya.
Kill switch adalah feature flag dengan satu tugas saja, yaitu mematikan sebuah fitur secara langsung ketika terjadi masalah di production, tanpa memerlukan rollback atau deployment baru. Evaluator yang ditunjukkan sebelumnya memeriksa kill switch sebelum apa pun yang lain, termasuk persentase rollout dan penargetan tenant, karena saat insiden terjadi kamu ingin satu tuas yang bisa mengalahkan semua aturan lain secara instan.
Disiplin yang membuat kill switch benar-benar berguna adalah memutuskan sejak awal jalur kode mana yang membutuhkannya. Membungkus setiap fitur baru dengan flag itu berlebihan, tetapi perubahan apa pun yang menyentuh billing, pemanggilan API eksternal dengan rate limit, atau migrasi data yang berjalan saat request, sebaiknya dirilis di balik flag yang bisa kamu balik dari dashboard atau satu panggilan API, bukan perubahan yang harus kamu deploy ulang untuk membatalkannya.
Uji jalur kill switch di staging sebelum fitur dirilis, bukan setelah sebuah insiden memaksamu memakainya untuk pertama kali di production. Kill switch yang belum pernah diuji coba adalah rencana rollback yang cuma kamu harap-harap berhasil.
Setiap flag yang kamu tambahkan adalah sebuah percabangan di codebase yang harus dirawat seseorang sampai flag itu dihapus. Backend dengan flag yang menumpuk selama dua tahun, kebanyakan sudah rollout seratus persen dan terlupakan, jauh lebih sulit dipahami dibanding backend tanpa flag sama sekali, karena setiap jalur kode harus dibaca sambil bertanya apakah bagian ini masih bercabang berdasarkan sesuatu.
Diperlakukan seperti ini, feature flag tetap menjadi alat rilis alih-alih berubah menjadi sistem konfigurasi kedua yang informal dan tidak sepenuhnya dipahami siapa pun. Tujuannya selalu sama, yaitu merilis secara bertahap, pulih secara instan jika ada yang rusak, dan meninggalkan codebase yang lebih sederhana setelah rollout selesai dibanding jika memakai deployment sekaligus dalam satu waktu.