← Semua pembelajaran / Go Nol → Enterprise
Fase 11 · Enterprise & Capstone

Feature flag — melepas kode tanpa menyalakan fitur

Deploy dan rilis adalah dua hal berbeda. Feature flag membuat kode baru bisa masuk produksi dalam keadaan mati, dinyalakan bertahap, dan dimatikan dalam hitungan detik saat bermasalah — tanpa deploy.

Intisari

  • Flag mengubah rollback dari "deploy ulang 10 menit" jadi "matikan saklar, 10 detik".
  • Empat jenis: rilis, operasional (saklar darurat), eksperimen, dan izin.
  • Flag rilis harus berumur pendek — hapus setelah fitur stabil, atau kodemu jadi labirin percabangan.
  • Evaluasi flag harus cepat dan tidak pernah gagal: cache lokal, dengan nilai bawaan yang aman.
  • AWS AppConfig menyediakannya secara terkelola; tabel database sederhana sudah cukup untuk memulai.

Antarmuka yang sederhana

type Flag interface {
	Aktif(ctx context.Context, nama string) bool
}

// Pemakaian: percabangan yang jelas dan mudah dihapus nanti.
func (h *Handler) Checkout(w http.ResponseWriter, r *http.Request) {
	if h.flag.Aktif(r.Context(), "checkout-baru") {
		h.checkoutBaru(w, r)
		return
	}
	h.checkoutLama(w, r)
}
// Implementasi: cache lokal yang disegarkan berkala, JANGAN memanggil
// jaringan di jalur permintaan.
type FlagCache struct {
	nilai   atomic.Pointer[map[string]Aturan]   // pembaca tanpa lock (Fase 2)
	sumber  Sumber
	log     *slog.Logger
}

func (f *FlagCache) Aktif(ctx context.Context, nama string) bool {
	m := f.nilai.Load()
	if m == nil {
		return false            // BAWAAN AMAN: fitur baru mati
	}
	a, ada := (*m)[nama]
	if !ada {
		return false
	}
	return a.Evaluasi(ctx)
}

func (f *FlagCache) segarkanBerkala(ctx context.Context) {
	t := time.NewTicker(30 * time.Second)
	defer t.Stop()
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			m, err := f.sumber.Muat(ctx)
			if err != nil {
				// Sumber flag bermasalah: PERTAHANKAN nilai lama.
				f.log.WarnContext(ctx, "gagal memuat flag", "err", err)
				continue
			}
			f.nilai.Store(&m)
		}
	}
}

Dua keputusan yang menjaga flag tidak jadi sumber pemadaman: evaluasi tidak pernah menyentuh jaringan (nilai di memori, disegarkan di latar), dan kegagalan memuat mempertahankan nilai terakhir alih-alih mengosongkannya. Sistem feature flag yang bisa menjatuhkan aplikasi saat ia sendiri bermasalah adalah ironi yang mahal.

Empat jenis flag

JenisUmurContoh
RilisHari–minggu, lalu dihapuscheckout-baru
OperasionalPermanenmatikan-rekomendasi — saklar darurat
EksperimenSelama pengujianA/B tata letak halaman produk
IzinPermanenfitur-enterprise per paket langganan

Flag rilis yang tidak pernah dihapus adalah utang teknis yang berlipat. Sepuluh flag berarti sampai 1024 kombinasi jalur kode — yang tidak satu pun diuji seluruhnya. Buat aturan: setiap flag rilis punya tanggal kedaluwarsa di komentarnya, dan CI memperingatkan flag yang lewat tanggal itu.

Peluncuran bertahap

type Aturan struct {
	Aktif      bool
	Persentase int       // 0–100
	Pengguna   []int64   // daftar putih: tim internal dulu
	Tenant     []int64
}

func (a Aturan) Evaluasi(ctx context.Context) bool {
	if !a.Aktif {
		return false
	}

	u := auth.DariContext(ctx)
	if slices.Contains(a.Pengguna, u.ID) {
		return true
	}

	if a.Persentase >= 100 {
		return true
	}
	if a.Persentase <= 0 {
		return false
	}

	// Hash ID pengguna, BUKAN acak: pengguna yang sama harus selalu
	// mendapat hasil yang sama, atau tampilannya berubah tiap muat ulang.
	h := fnv.New32a()
	fmt.Fprintf(h, "%s:%d", a.Nama, u.ID)
	return int(h.Sum32()%100) < a.Persentase
}
Urutan peluncuran yang lazim:

  1. Deploy dengan flag MATI            → kode di produksi, tidak aktif
  2. Nyalakan untuk tim internal        → uji dengan data sungguhan
  3. 5% pengguna, pantau alarm          → p95, laju error, metrik bisnis
  4. 25% → 50% → 100%
  5. Hapus flag dan kode jalur lama     ← jangan dilewati

Menguji kedua jalur

func TestCheckout(t *testing.T) {
	for _, aktif := range []bool{false, true} {
		t.Run(fmt.Sprintf("flag=%v", aktif), func(t *testing.T) {
			h := New(svcUji(t), flagTetap{"checkout-baru": aktif})
			...
		})
	}
}

// Flag palsu: satu map, tanpa pustaka.
type flagTetap map[string]bool

func (f flagTetap) Aktif(_ context.Context, nama string) bool { return f[nama] }

Kalau hanya jalur baru yang diuji, flag-mu bukan jaring pengaman. Seluruh nilai feature flag ada pada kemampuan mematikannya saat produksi bermasalah — dan itu berarti jalur lama harus tetap bekerja. Uji keduanya sampai flag-nya dihapus.

Pilihan penyimpanan

SumberCocok kalau
Tabel database + cache 30 detikTitik awal. Nol layanan baru, sudah cukup untuk sebagian besar
AWS AppConfigButuh validasi, peluncuran bertahap terkelola, dan rollback otomatis berbasis alarm
Parameter StoreFlag sederhana, gratis (Fase 10)
Layanan pihak ketigaButuh eksperimen dan analitik yang matang
Environment variableBukan flag — mengubahnya butuh deploy

Kelebihan AppConfig yang paling relevan: ia bisa memantau alarm CloudWatch selama peluncuran dan membatalkan sendiri kalau laju error naik. Itu menggabungkan flag dengan observability yang sudah kamu bangun di Fase 9 — peluncuran yang mengawasi dirinya sendiri.

Latihan: pasang antarmuka Flag dengan penyimpanan tabel database dan cache 30 detik, lalu bungkus satu endpoint dengan flag. Nyalakan untuk 10% pengguna dan buktikan pengguna yang sama selalu mendapat jalur yang sama. Terakhir, matikan sumber flag-nya dan pastikan aplikasi tetap berjalan dengan nilai terakhir.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.