Một team khi nghe “feature flag” thường nghĩ tới if (FEATURE_X) { ... } và dùng nó y như một biến môi trường. Kết quả vài tháng sau: code rải rác flag chết, không ai dám xoá, behavior tuỳ môi trường khó test. Nguyên do: nhầm giữa feature flag và runtime configuration.
Bài này phân biệt rõ hai khái niệm, chỉ ra bốn loại flag chính, cách targeting, cách đo, và quan trọng nhất — làm sao xoá flag khi xong việc.
1. Định nghĩa lại hai khái niệm
1.1. Runtime configuration
Các tham số ổn định, bán cố định mà hệ thống cần để chạy:
- Database URL, Redis URL.
- Feature mode chung theo môi trường (dev/staging/prod).
- Timeout, retry count.
- Giới hạn quota mặc định.
Đặc điểm:
- Thường đọc một lần lúc boot, không đổi trong khi chạy.
- Tập hợp nhỏ, ít người đụng, người đụng là SRE/DevOps.
- Đổi giá trị → restart dịch vụ hoặc dùng tool reload nhẹ.
- Lưu trong
.env, ConfigMap, Vault, AWS Parameter Store.
1.2. Feature flag
Quyết định sản phẩm có thể thay đổi trong lúc chạy, theo user/context:
- Bật tính năng mới cho 5% user.
- Cho tenant cao cấp thấy tab “Analytics”.
- Tạm tắt endpoint đắt khi downstream quá tải.
- A/B test giao diện checkout mới.
Đặc điểm:
- Đọc mỗi request (hoặc mỗi session), có thể khác nhau theo user.
- Thay đổi không cần deploy.
- Đo lường kết quả (metrics, analytics) gắn với flag.
- Số lượng có thể hàng trăm trong product trưởng thành.
- Lưu trong feature flag service (LaunchDarkly, Unleash, Flagsmith) hoặc hệ nội bộ.
1.3. So sánh ngắn
| Khía cạnh | Config | Feature flag |
|---|---|---|
| Đối tượng thay đổi | SRE, ít người | PM, eng, rollout owner |
| Phạm vi | Toàn dịch vụ | Per-user/tenant/request |
| Tần suất đổi | Thấp | Cao, hằng ngày |
| Vòng đời | Dài | Ngắn — phải xoá sau khi xong |
| Lưu | ConfigMap, Vault | Flag service |
| Có SDK targeting? | Không | Có |
Lẫn hai → hoặc config dài dòng với targeting lung tung, hoặc flag không bao giờ xoá.
2. Bốn loại feature flag (Pete Hodgson)
Theo phân loại kinh điển của Pete Hodgson (martinfowler.com), flags khác nhau về vòng đời và người sở hữu:
2.1. Release toggle
- Mục tiêu: tách deploy khỏi release. Merge code vào main nhưng flag tắt; khi ready, bật dần.
- Vòng đời: ngắn (vài ngày–vài tuần). Xoá sau khi ra 100%.
- Sở hữu: engineer viết feature.
2.2. Experiment toggle
- Mục tiêu: A/B test. Chia user random theo nhóm, đo metric.
- Vòng đời: trung bình (chạy đủ lâu để có ý nghĩa thống kê).
- Sở hữu: PM / data team.
- Xoá sau khi có kết luận, áp dụng winner cho tất cả.
2.3. Ops toggle
- Mục tiêu: điều khiển vận hành. Tắt tính năng đắt khi downstream hỏng, bật mode degrade, giới hạn QPS.
- Vòng đời: dài, có thể vĩnh viễn (circuit breaker, kill switch).
- Sở hữu: SRE.
2.4. Permission toggle
- Mục tiêu: phân quyền theo plan, tenant, beta program.
- Vòng đời: dài, gắn với product tier.
- Sở hữu: product / billing.
Hầu hết bug phát sinh khi nhầm loại: dùng release toggle như permission (không xoá), dùng permission như ops (dư thừa logic), dùng experiment như release (không đo).
3. Kiến trúc triển khai
3.1. Các thành phần
- Flag store: nơi lưu rule & giá trị. Có thể là DB, Redis, hay SaaS.
- Control plane / UI: nơi PM/eng sửa flag, xem audit.
- SDK/Client trong app: đánh giá flag dựa trên context.
- Event pipeline: ghi lại flag evaluation để debug + đo.
3.2. Đánh giá phía server vs phía client
- Server-side: an toàn — client không thấy flag chưa bật; quyết định dựa vào user context tin cậy.
- Client-side (web/mobile): rule phải gửi đến client. Ai đọc bundle thấy tên flag. Không bao giờ để quyền bảo mật (admin, payment) phụ thuộc vào flag client-side.
Pattern tốt: server trả về kết quả (bật/tắt) cho mỗi flag, không phải rule.
3.3. Caching và performance
Mỗi request đánh giá flag không được gọi SaaS flag bên ngoài — latency cao. Thay bằng:
- SDK stream rule xuống local, đánh giá in-process.
- Cache kết quả per-user trong request (tránh tính lại nhiều lần).
- Hệ thống ổn khi flag service chết: default value rõ ràng (flag failsafe).
3.4. Consistency
Trong một user session, flag không nên lật đi lật lại. Dùng hash theo user_id để đảm bảo stable. Khi đổi rule (ví dụ từ 5% → 10%), user đã trong nhóm không bị văng ra (sticky bucketing).
4. Targeting: bật cho ai
Một rule flag thực tế gồm:
flag: new_checkout_ui
default: off
rules:
- if: user.tenant in ["acme", "globex"] # beta tester
value: on
- if: user.country == "VN" and random(user.id) < 0.1
value: on
- value: off
Các khoá targeting phổ biến:
- user.id: per-user rollout (phần trăm).
- user.tenant / org: theo khách hàng (enterprise).
- user.country / locale: rollout theo vùng.
- user.plan: free/pro/enterprise.
- env: dev/staging/prod.
- time: bật theo khung giờ.
Giữ context tối thiểu đủ để rule chạy. Đừng đẩy PII (email, phone) vào — ngoài vi phạm privacy, flag log của vendor sẽ chứa PII.
5. Đo lường
5.1. Exposure logging
Mỗi lần flag evaluate cho một user, ghi lại (async):
{
"user_id_hash": "a1b2c3",
"flag": "new_checkout_ui",
"variant": "on",
"ts": 1712800000
}
Gửi vào data warehouse (BigQuery, Snowflake) hoặc analytics. Đây là nền để so sánh metric giữa nhóm “on” vs “off”.
5.2. Metric + flag
Trong Prometheus, không thêm flag làm label nếu tạo nhiều chuỗi. Tốt hơn: log exposure ở data pipeline, join với event (conversion, revenue) ngoại tuyến.
5.3. Guardrail metrics
Khi rollout, theo dõi guardrail: error rate, latency, conversion funnel. Tự động rollback nếu guardrail vượt ngưỡng — nhiều flag service hỗ trợ automated rollback.
6. Pattern code sạch
6.1. Wrapper / gate pattern
Thay vì if (flag.enabled("new_checkout")) rải khắp code:
class CheckoutService {
checkout(ctx: Ctx, cart: Cart): Result {
if (this.flags.isOn("new_checkout", ctx.user)) {
return this.newCheckout(ctx, cart);
}
return this.oldCheckout(ctx, cart);
}
}
Tách branch vào method — dễ xoá sau này (chỉ delete method cũ, remove if).
6.2. Strategy / DI
const strategy = flags.variant("checkout_algo", ctx.user);
const impl = checkoutStrategies[strategy] ?? checkoutStrategies.default;
impl.run(cart);
Phù hợp khi có nhiều variant (không chỉ on/off) hoặc flag lâu dài (permission).
6.3. Test với flag
- Unit test cho cả hai nhánh (hoặc mọi variant) — dùng mock flag.
- Integration test: matrix
{flag=on, flag=off}. - Nếu flag là release toggle sắp xoá, đừng sa đà viết quá nhiều test cho nhánh cũ.
7. Vòng đời flag và chống tech debt
Số 1 vấn đề của feature flag: không ai xoá.
7.1. Đặt “ngày hết hạn”
Mỗi flag tạo mới có owner và expected removal date trong metadata. Alert khi quá hạn.
7.2. Báo cáo flag
- Flag service tốt có view: flag nào > 90 ngày chưa thay đổi, flag nào 100% on/off liên tục → ứng viên xoá.
- CI linter quét code: flag nào dùng < 1 lần, hay flag nào có cả hai nhánh code nhưng rule đã 100%.
7.3. Quy trình xoá
- Xác nhận flag ở 100% trong prod đủ lâu (vài tuần).
- Xoá flag khỏi flag store (hoặc đánh dấu archive).
- Trong PR: remove if/else, giữ nhánh mới, xoá code cũ.
- Remove exposure logging, test liên quan.
- Cập nhật tài liệu nếu có.
7.4. Hạn mức flag
Đặt quota: “không quá N flag sống cùng lúc cho team X”. Khi chạm trần, buộc phải dọn trước khi thêm mới.
8. Khi nào không cần feature flag
- Thay đổi cực nhỏ (đổi text, fix bug đơn giản): deploy thẳng. Thêm flag tạo overhead.
- Migration backend không có user-visible change.
- Thử nghiệm ở staging/test environment: dùng env config là đủ.
Mỗi flag là một trạng thái bạn phải test, log, và sau này xoá. Nếu giá trị < chi phí → đừng dùng.
9. Feature flag cho database migration
Một ứng dụng đắt giá: expand-migrate-contract.
- Deploy code mới có flag (off). Code mới ghi cả schema cũ và mới (dual-write) sau flag.
- Backfill dữ liệu schema mới.
- Bật flag: đọc từ schema mới, vẫn ghi cả hai.
- Sau khi ổn, stop ghi schema cũ (flag mới hoặc release lần nữa).
- Drop schema cũ.
Flag cho phép bật từng bước, rollback dễ — migration an toàn hơn hẳn so với “deploy one shot”.
10. Các cạm bẫy nữa
- Phụ thuộc flag cho bảo mật: flag là UX toggle, không phải quyền. Dùng authz đúng (RBAC/ABAC).
- Flag tạo ra spaghetti state: 5 flag → 32 tổ hợp. Hạn chế số flag đồng thời cho cùng một luồng code.
- Không có audit: ai đổi flag lúc nào? Khi incident, audit là vàng. Flag service phải log mọi thay đổi.
- Khác nhau client vs server: ứng dụng mobile cần grace period (cached flag) giữa các phiên bản — xử lý thận trọng khi đổi rule.
11. Tóm tắt
- Config runtime và feature flag khác nhau về mục đích, vòng đời, người sở hữu. Đừng trộn.
- Bốn loại flag (release, experiment, ops, permission) có vòng đời riêng → quyết định cách viết & xoá.
- Server-side evaluation là default an toàn; client chỉ nhận kết quả.
- Targeting dựa trên context tối thiểu; sticky bucketing cho consistency.
- Exposure logging + guardrail metrics để rollout an toàn.
- Wrapper/strategy pattern giúp xoá flag dễ dàng sau này.
- Quản lý vòng đời: owner, hạn chót, quota — nếu không thì flag thành nợ kỹ thuật.
Feature flag đúng cách là công cụ giảm rủi ro. Dùng sai thành nguồn bug mới và mảng code chết. Sự khác biệt nằm ở kỷ luật dọn dẹp, không ở tool — dù dùng SaaS đắt tiền hay hệ nội bộ đơn giản.