Feature Flag؛ از یک if ساده‌، تا چیزی که کسی جرئت حذفش رو نداره!

در حالی‌که یه عده بحث می‌کنن که درد عاشقی بدتره یا درد بی‌پولی، هم‌زمان عده‌ای از لابلای صف دستشویی بهشون پوزخند می‌زنن؛ ولی یه گروه چهارمی هم هستن که یه قابلیت جدید برای نرم‌افزارشون نوشتن، ولی نمی‌دونن چجوری فعال/غیرفعال شدنش رو بدون ریلیز جدید، کنترل کنن! بله، این گروه‌، همون‌هایی هستن که یا feature flag رو نمی‌دونن چیه؛ یا درست پیاده‌سازی کردنش رو بلد نیستن؛ و در شرایط بحرانی آرزو می‌کردن توی صف قضای حاجت بودن، ولی درگیر اون پروژه نبودن؛ ولی از این گروه بدبخت‌تر هم داریم؛ یعنی کسانی که یه کدبیس بهشون ارث رسیده که ده‌ها یا صدها feature flag رها شده داره…

فرض کنین یه قابلیت جدید نوشتید، ولی هنوز اون‌قدری بهش مطمئن نیستید که یک‌باره در اختیار همه کاربرانتون قرارش بدید. ساده‌ترین راه‌حل براش اینه که پشت یک Feature Flag بگذاریدش، مثلاً:

if (featureFlags.NewCheckoutEnabled)
{
    return NewCheckout();
}

return OldCheckout();

ایده و پیاده‌سازی هر دو ساده و سرراسته. کد رو Deploy می‌کنید، ولی خود قابلیت رو هر وقت آماده بودید Release می‌کنید. مثلاً اول برای اعضای تیم، بعد برای یک درصد از کاربرها و نهایتاً برای همه. اگه هم مشکلی پیش اومد، بدون Deploy مجدد، قابلیت رو خاموش می‌کنید.

اما مشکل از خود Feature Flag شروع نمی‌شه. مشکل از روزی شروع می‌شه که چند ماه بعدش یکی می‌پرسه: «این Flag هنوز لازمه؟» و پاسخ تیم چیزی شبیه اینه: «فعلاً دستش نزن؛ دقیق نمی‌دونیم چه چیزهایی بهش وابسته‌اند!»

از همین‌جاست که یک ابزار مفید برای کاهش ریسک، آروم‌آروم به بدهی فنی تبدیل می‌شه! من بارها و بارها توی کدبیس‌های بزرگ و تیم‌هایی که گردش نیرو داشتن؛ این موضوع رو دیدم که حذف یه feature flag ساده، ارتباط مستقیم با شجاعت و شهامت افراد پیدا کرده به جای اینکه به اطلاعشون از نیاز به اون قابلیت مرتبط بشه!

Feature Flag فقط یه کلید روشن و خاموش نیست

Feature Flag کمک می‌کنه Deployment رو از Release جدا کنیم.

ممکنه که کد یک قابلیت امروز وارد Production بشه، اما خود قابلیت هفته آینده فقط برای کارکنان داخلی فعال بشه. چند روز بعد ۵ درصد کاربران اون رو ببینن و اگه شاخص‌های خطا و کارایی مناسب بود، به‌تدریج برای همه فعال بشه.

این قابلیت چند استفاده مهم داره:

  • انتشار تدریجی و کاهش شعاع خرابی
  • آزمایش قابلیت روی گروه محدودی از کاربران
  • اجرای A/B Test
  • خاموش‌کردن فوری یک رفتار پرخطر
  • فعال‌سازی یک قابلیت برای مشتری، کشور یا گروه مشخص
  • ارائه کد ناقص یا غیرفعال، بدون نمایشش به کاربران

اما همه این Flagها یک ماهیت و یک طول عمر ندارن. اگه با همه اون‌ها یکسان رفتار کنیم، خیلی زود کنترلشون رو از دست می‌دیم.

همه Flagها قرار نیست برای همیشه باقی بمونن

برای مدیریت درست، اول باید بدونیم با چه نوع Flagی طرف هستیم.

۱. Release Flag

برای انتشار تدریجی یک قابلیت جدید ساخته می‌شه. وقتی قابلیت، پایدار شد و به همه کاربرها رسید، مسیر قدیمی و خود Flag باید حذف بشن. این Flag ذاتاً موقتیه.

۲. Experiment Flag

برای A/B Test یا مقایسه چند رفتار استفاده می‌شه. بعد از پایان آزمایش و انتخاب نسخه برنده، دیگه دلیلی برای باقی‌موندن شاخه‌های آزمایشی وجود نداره.

این Flag هم تاریخ انقضا داره، حتی اگه موقع ساختنش کسی تاریخ دقیق پایان آزمایش رو ندونه.

۳. Operational Flag یا Kill Switch

برای کنترل رفتار سیستم توی شرایط عملیاتیه. مثلاً اگر سرویس بیرونی دچار اختلال شد، بتونیم ارسال درخواست بهش رو سریع متوقف کنیم.

این جور Flagها ممکنه دائمی باشن، چون قابلیت خاموش‌کردن اضطراری، خودش بخشی از طراحی سیستمه.

۴. Entitlement یا Permission

گاهی قابلیت، فقط برای مشتری‌های یک پلن، کارکنان یک منطقه یا کاربران دارای مجوز مشخص فعال می‌شه. این وضعیت ممکنه دائمی هم باشه، اما سؤال مهم اینه: «آیا این واقعاً Feature Flag است یا بخشی از مدل دسترسی و قوانین کسب‌وکار؟»

و اگه دسترسی کاربر به یک قابلیت، جزئی از قرارداد محصوله، احتمالاً بهتره اون رو به‌عنوان Entitlement، Permission یا Policy مدل کنیم، نه یک Flag که توی کد پراکنده شده.

۵. Remote Configuration

گاهی هم قرار نیست قابلیتی روشن یا خاموش بشه، بلکه می‌خوایم مقدار Timeout، تعداد Retry، اندازه Batch یا یک رفتار قابل تنظیم رو تغییر بدیم.

این هم لزوماً Feature Flag نیست. تبدیل تمام تنظیمات سیستم به Flag، فقط یک مخزن جدید از تصمیم‌های مبهم می‌سازه. پس «دائمی‌بودن» یک Flag الزاماً اشتباه نیست. مشکل زمانیه که تفاوت بین Feature Flag، Kill Switch، Configuration، Permission و Business Policy رو نادیده بگیریم.

وقتی تعداد ifها از کنترل خارج می‌شن

فرض کنید یک قابلیت به سه Flag وابسته باشه:

if (newCheckout && newPaymentProvider && fraudDetectionV2)
{
    // ...
}

هر Flag جدید، فقط یک شرط دیگه اضافه نمی‌کنه. ترکیب رفتارهای ممکن رو هم افزایش می‌ده. به‌صورت تئوری، n تا فلگ Boolean، می‌تونن تا 2^n ترکیب بسازن.

یعنی ۱۰ تا Flag می‌تونه ۱۰۲۴ تا ترکیب رفتاری ایجاد کنه. طبیعتاً همه این حالت‌ها در عمل معتبر نیستن، اما همین مسئله چند تا سؤال سخت به وجود میاره:

  • کدوم یک از ترکیب‌ها واقعاً توی Production فعال می‌شن؟
  • کدوم‌یکی رو باید تست کنیم؟
  • اگر Flag اول خاموش باشه و Flag سوم روشن باشه، چی می‌شه؟
  • آیا QA، توسعه‌دهنده، و کاربر در یک لحظه رفتار یکسانی می‌بینن؟
  • آیا خاموش‌کردن یک Flag، سیستم رو واقعاً به وضعیت قبلی برمی‌گردونه؟

حالا اگه ارزیابی Flag در سطح چند Controller، Service، Component و Query پراکنده شده باشه، حذفشون دیگه یک تغییر ساده نیست. تیم باید همه شاخه‌ها رو پیدا کنه و حدس بزنه کدوم مسیر هنوز زنده است.

توی چنین شرایطی معمولاً کسی Flag رو حذف نمی‌کنه. مسیر قدیمی و جدید سال‌ها کنار هم باقی می‌مونن و هر تغییر تازه باید هر دو رو در نظر بگیره.

Flag موقت باید واقعاً موقت باشه

یکی از اشتباهات رایج اینه که تیم، فعال‌شدن قابلیت برای ۱۰۰ درصد کاربرها رو پایان کار می‌دونه! ولی اگه Release Flag به ۱۰۰ درصد رسیده و قرار نیست دوباره خاموش بشه، کار هنوز تموم نشده. باید:

  1. مسیر قدیمی حذف بشه.
  2. شرط Flag از کد بیرون بیاد.
  3. تست‌های مربوط به رفتار منسوخ، پاک یا بازنویسی بشن.
  4. خود Flag از پلتفرم مدیریت Flag حذف بشه.
  5. مستندات و داشبوردهای وابسته بهش هم اصلاح بشن.

حذف Flag باید بخشی از Definition of Done همون قابلیت باشه، نه یه کار اختیاری برای «بعداً».

یک قاعده ساده می‌تونه کمک کنه: «هر Flag موقت باید از لحظه تولد، مالک، تاریخ بازبینی و Ticket حذف داشته باشه.» و اگه ندونیم چه کسی باید اون رو حذف کنه، در واقع از همون اول تصمیم گرفتیم که باقی بمونه.

چرا app settings یا جدول تنظیمات کافی نیست؟

توی یه پروژه کوچیک، احتمالاً می‌شه چند تا Flag رو توی فایل تنظیمات یا دیتابیس نگه داشت. اما با بزرگ‌شدن تیم و سیستم، مسئله دیگه فقط نگهداری مقدار true و false نیست.

باید بدونیم:

  • چه کسی Flag رو تغییر داده؟
  • مقدار قبلی چی بوده؟
  • چه کسی اجازه تغییر Flag رو توی Production داره؟
  • Flag توی محیط Development، Stage، Test و Production چه وضعیتی داره؟
  • آیا قراره برای کاربر، گروه، کشور یا درصد مشخصی فعال بشه؟
  • آیا تغییر باید فوراً اعمال بشه یا طبق برنامه پیش بره؟
  • اگه سرویس مدیریت Flag در دسترس نبود، رفتار امن برنامه چیست؟
  • کدوم Flagها منقضی یا بدون استفاده شدن؟

اینجاست که داشتن یک پلتفرم مدیریت Feature Flag اهمیت پیدا می‌کنه. و پلتفرم خوب فقط یک صفحه خوشگل برای روشن و خاموش‌کردن Flag نیست. باید امکاناتی مثل Targeting، Percentage Rollout، Audit Log، RBAC، تاریخ انقضا، محیط‌های مجزا، مشاهده وضعیت و کنترل دسترسی رو هم فراهم کنه.

چه پلتفرم‌هایی وجود دارن؟

قرار نیست یک ابزار برای همه تیم‌ها بهترین انتخاب باشه. نیاز یه استارتاپ کوچیک با یه شرکت با چند صد تا سرویس، یکسان نیست. با این حال، چند پلتفرم شناخته‌شده، نماینده رویکردهای متفاوت‌ می‌تونن باشن.

LaunchDarkly

LaunchDarkly یه پلتفرم تجاری و تخصصی شناخته‌شده برای مدیریت Flagها، هدف‌گیری کاربرها و Segmentها، انتشار تدریجی و Experimentation است. امکانات خوبی مثل تغییرات زمان‌بندی‌شده، Percentage Rollout و کنترل متمرکز اون‌ها رو هم برای سازمان‌هایی که فرایند انتشار پیچیده دارن فراهم می‌کنه.

Unleash

Unleash گزینه‌ی Open Source با امکان Self-hosting است. برای تیم‌هایی جذابه که کنترل بیشتری روی زیرساخت و داده‌هاشون نیاز دارن یا دسترسی به سرویس ابری و امکان پرداخت هزینه‌هاش رو ندارن.

Flagsmith

Flagsmith علاوه بر اینکه Feature Flag، نیازهایی مثل Remote Configuration رو هم پوشش می‌ده و مدل‌های Cloud، Self-hosted و On-premises داره. این انعطاف برای سازمان‌هایی که محدودیت استقرار یا امنیتی دارن مفیده.

GrowthBook

GrowthBook برای تیم‌هایی جذابه که Feature Flag رو همراه با Experimentation و A/B Testing می‌خوان. تمرکزش فقط روی روشن و خاموش‌کردن قابلیت نیست و می‌تونه تأثیرات هر تغییر روی معیارهای محصول رو هم وارد تصمیم‌گیری کنه.

Azure App Configuration و AWS AppConfig

اگر زیرساخت تیم عمدتاً روی یه Cloud مشخص قرار داشته باشه، سرویس‌های بومی همون Cloud هم انتخاب‌های منطقی‌ می‌تونن باشن. مثلاً Azure App Configuration مدیریت متمرکز و انتشار هدفمند Flagها رو ارائه می‌ده؛ یا AWS AppConfig هم Feature Flag و Dynamic Configuration رو توی اکوسیستم AWS فراهم می‌کنه.

OpenFeature

OpenFeature خودش پلتفرم مدیریت Flag نیست. یک استاندارد و API مستقل از Vendor است که بین کد برنامه و Provider قرار می‌گیره. در حالت عادی ممکنه کد برنامه مستقیماً به SDK یک شرکت وابسته بشه. OpenFeature تلاش می‌کنه تا API ارزیابی Flag رو استاندارد کنه تا تغییر Provider یا استفاده از راهکار داخلی، الزاماً به بازنویسی تمام نقاط کد منجر نشه.

این استاندارد مشکل Vendor Lock-in رو کاملاً حذف نمی‌کنه، چون قابلیت‌های پلتفرم‌ها دقیقاً یکسان نیست، ولی دست‌کم وابستگی مستقیم کد به SDK اختصاصی رو کاهش می‌ده.

موقع انتخاب پلتفرم به چه چیزهایی توجه کنیم؟

تعداد قابلیت‌های صفحه محصول معیار کافی نیست. چند سؤال عملی مهم‌تره:

  • آیا باید Self-host باشه یا SaaS قابل قبوله؟
  • SDK رسمی و قابل اعتماد برای استک تکنولوژی تیم داره؟
  • ارزیابی Flag سمت Client انجام می‌شه یا Server؟
  • موقع قطعی شبکه یا خود پلتفرم چه اتفاقی می‌افته؟ (مثلاً لایه cache آفلاین داره؟)
  • آیا مقدار Flag به‌صورت محلی Cache می‌شه؟
  • تغییرات چقدر سریع به همه Instanceها می‌رسن؟
  • آیا Audit Log، RBAC و Approval داره؟
  • آیا Flagهای قدیمی و بدون استفاده رو شناسایی می‌کنه؟
  • آیا Percentage Rollout و Targeting قابل پیش‌بینی و پایداره؟
  • هزینه‌اش با افزایش کاربر، درخواست و Environment چجوری رشد می‌کنه؟
  • مهاجرت ازش/بهش چقدر دشواره؟

مهم‌تر از همه، پلتفرم باید با فرایند تیم هماهنگ باشه. اگه ابزار بتونه Flag منقضی‌شده رو تشخیص بده اما تیم هیچ‌وقت هشدارهاش رو بررسی نکنه، عملاً چیزی حل نشده!

پلتفرم، جای Governance رو نمی‌گیره

خریدن یا نصب‌کردن یه پلتفرم Feature Flag، به‌خودی‌خود بدهی فنی رو از بین نمی‌بره. ابزار، فقط امکان مدیریت رو فراهم می‌کنه؛ ولی خود مدیر رو نه! برای هر Flag حداقل باید این اطلاعات مشخص باشه:

  • نام و هدف شفاف و قابل فهم
  • نوع Flag
  • مالک فنی
  • مالک محصول یا کسب‌وکار، در صورت نیاز
  • تاریخ ایجاد
  • تاریخ بازبینی یا انقضا
  • رفتار پیش‌فرض و امن در زمان خطا
  • محیط‌های مجاز
  • برنامه Rollout
  • شرایط موفقیت یا شکست
  • Ticket و برنامه حذف

همین‌طور بهتره تا ارزیابی روشن یا خاموش بودن Flag تا جای ممکن توی یک جا؛ یا یک سطح مشخص و نزدیک به مرز رفتار انجام بشه، نه اینکه شرط فعال/غیرفعال بودنش توی ده‌ها متد پراکنده بشه.

مثلاً به‌جای اینکه تمام اجزای قابلیت از وجود Flag خبر داشته باشن، می‌شه توی سطح بالاتری تصمیم گرفت کدوم پیاده‌سازی استفاده بشه:

ICheckoutService checkoutService = featureFlags.NewCheckoutEnabled
    ? new NewCheckoutService()
    : new LegacyCheckoutService();

این کار تعداد نقاط تصمیم‌گیری رو کم می‌کنه و حذف مسیر قدیمی رو ساده‌تر نگه می‌داره.

چالش‌های Feature Flag در مقیاس سازمانی

Consistency توی سیستم‌های توزیع‌شده: وقتی هر سرویس یه حقیقت متفاوت می‌بینه

فرض کنید یک درخواست Checkout از سه تا سرویس عبور می‌کنه: API Gateway، Payment Service و Notification Service. اگه هرکدوم از این سرویس‌ها مستقلاً و در لحظه‌های متفاوت، مقدار NewCheckoutEnabled رو بررسی کنن، ممکنه چنین اتفاقی بیفته:

  • Gateway هنوز نسخه قدیمی رو می‌بینه، چون دیرتر Sync شده. و Payment Service همون لحظه، نسخه جدید رو می‌بینه. و نتیجه؟ یک درخواست که نصفش از مسیر با لاجیک قدیمی طی شده، و نصف دیگه‌اش با لاجیک جدید.

این نوع ناسازگاری معمولاً خودش رو جایی نشون می‌ده که کمترین انتظار رو داری، مثلاً: یک Payment که توی سیستم قدیمی Authorize شده، اما توی سیستم جدید Capture می‌شه، یا یه Notification که برای یه قابلیتی ارسال می‌شه که کاربر اصلاً هنوز اون رو ندیده اصلاً.

یکی از راه‌حل‌ها اینه که تصمیم توی اولین نقطه‌ای که Context کافی داره، فقط یک‌بار گرفته و به‌صورت Metadata داخلی و غیرقابل‌دستکاری به مراحل بعدی منتقل بشه. در درخواست‌های HTTP، لبه سیستم باید هر مقدار مشابهی رو از ورودی Client حذف و خودش بازنویسی کنه. در جریان‌های Async هم مقدار ارزیابی‌شده یا نسخه رفتار باید همراه پیام ذخیره بشه.:

// در لبه سیستم (Gateway یا اولین Entry Point)
var checkoutVersion = featureFlags.Evaluate("new-checkout", context);
request.Headers.Add("X-Checkout-Version", checkoutVersion);

و

// توی سرویس‌های پایین‌دستی
var version = request.Headers["X-Checkout-Version"];

این‌طوری، تصمیم فقط یک‌بار گرفته می‌شه و در طول عمر یک درخواست، همه سرویس‌ها همون تصمیم رو می‌بینن؛ حتی اگه خود Flag توی همون لحظه در حال تغییر باشه. مسئله شبیه همون چیزیه که توی الگوی Outbox/Inbox باهاش مواجه بودیم: تضمین یک دید واحد و پایدار از حقیقت، توی سیستمی که ذاتاً چند بخش مستقل داره.

و آیا باید همه ترکیب‌های Flagها را تست کنیم؟

توی سیستم‌های واقعی، معمولاً تست تمام ترکیب‌ها ممکن نیست. به همین دلیل باید تعداد Flagهای هم‌زمان و وابستگی بینشون رو محدود کنیم. حداقل لازمه تا این حالت‌ها پوشش داده بشن:

  • رفتار سیستم با مقدار پیش‌فرض Flag
  • مسیر روشن و خاموش هر Flag موقت
  • ترکیب‌های رسمی و پشتیبانی‌شده
  • رفتار موقع دردسترس‌نبودن Provider
  • رفتار Kill Switch در شرایط واقعی
  • ثبات تخصیص کاربر در Percentage Rollout و آزمایش‌ها

اگر دو تا Flag نباید هم‌زمان فعال باشن، این محدودیت نباید فقط توی ذهن اعضای تیم باشه. باید توی طراحی، اعتبارسنجی یا پلتفرم، قابل مشاهده و اِعمال‌کردن باشه.

و به‌جای تلاش برای تست 2^n ترکیب (که در عمل هم غیرممکنه و هم بی‌فایده)، سه تا تکنیک عملی وجود دارد:

Pairwise Testing: به‌جای تست همه ترکیب‌ها، فقط ترکیب هر دو Flag با هم رو پوشش می‌دیم. تحقیقات نشون می‌دن که باگ‌های واقعی متعددی، از تداخل دو تا عامل به وجود اومدن، نه تداخل هم‌زمان پنج یا شش تا عامل. ابزارهایی مثل PICT مایکروسافت می‌تونن از روی لیست Flagها، کوچک‌ترین مجموعه ترکیب‌های لازم برای پوشش Pairwise رو تولید کنن.

محدودکردن ترکیب‌های رسمی: به‌جای اجازه‌دادن به هر ترکیبی از Flagها، در لایه طراحی مشخص می‌کنیم کدوم ترکیب‌ها اصلاً «معتبر» هستن. اگر Flag سوم فقط زمانی معنا داره که Flag اول روشن باشه، این وابستگی باید توی پلتفرم Flag یا در سطح Validation کد اعمال بشه، نه فقط توی ذهن یک نفر در تیم.

public class FeatureFlagValidator {
public void ValidateConstraints(FlagState state) {
  if (state.FraudDetectionV2 && !state.NewPaymentProvider)
    throw new InvalidFlagCombinationException(
    "FraudDetectionV2 requires NewPaymentProvider");
    }
  }

Contract Testing بین سرویس‌ها: وقتی رفتار پشت یک Flag، روی مرز بین دو تا سرویس تأثیر می‌گذاره، یونیت‌تست توی هر سرویس کافی نیست. Contract Test تضمین می‌کنه که وقتی Flag روشنه، Payload یا Schema بین دو طرف هنوز سازگاره؛ مستقل از اینکه کدوم نسخه از منطق داخلی در حال اجراست.

Feature Flag به‌عنوان سطح حمله

Feature Flag معمولاً به چشم یک ابزار عملیاتی دیده می‌شه، نه یک سطح امنیتی. همین غفلت باعث می‌شه تا چند تا مسیر نفوذ واقعی ایجاد بشه.

افشای قابلیت‌های منتشرنشده: اگه ارزیابی Flag سمت Client انجام بشه(مثلاً توی اپلیکیشن موبایل یا کد جاوااسکریپت)، هر کاربری با کمی کنجکاوی می‌تونه لیست کامل Flagها و مقادیرشون رو توی Response API یا Bundle برنامه پیدا کنه؛ حتی اگه خود قابلیت هنوز برای هیچ‌کس فعال نشده باشه. نام یک Flag مثل enable-competitor-price-match به‌تنهایی می‌تونه یک اطلاعات محرمانه درز داده‌شده باشه.

دستکاری Targeting و ورود به گروه آزمایشی از طریق دستکاری Request: اگه تخصیص کاربر به یک Percentage Rollout بر اساس مقداری باشه که از سمت Client ارسال می‌شه (مثل یک Header یا Cookie قابل تغییر)، کاربر می‌تونه خودش رو به گروهی که یک قابلیت پرخطر یا آزمایشی براش فعاله، منتقل کنه. Feature Flag نباید جایگزین Authorization بشه. حتی اگه رابط کاربری پشت Flag پنهان شده، خود Endpoint و عملیات حساس باید مستقل از Flag، مجوز کاربر رو بررسی کنن.

سطح حمله در خود پلتفرم مدیریت Flag: دسترسی به داشبورد Flag معمولاً کمتر از دسترسی به کد یا زیرساخت محافظت می‌شه؛ در حالی که تغییر یک Flag می‌تونه مستقیماً منطق Authorization یا Fraud Detection رو خاموش کنه. RBAC و Audit Log روی این پلتفرم باید مثل دسترسی به Production Database، با همون جدیت محافظت بشه.

قاعده عملی: هر Flagی که افشا شدنش، اطلاعات تجاری حساس یا مسیر امنیتی را لو می‌ده، باید فقط سمت Server ارزیابی بشه؛ نه Client.

هزینه‌ای که معمولاً دیده نمی‌شه: Performance

ارزیابی درست Feature Flag معمولاً باید یک عملیات محلی و بسیار کم‌هزینه باشه. اگه هر بار برای بررسی Flag یک Network Call انجام بدیم، خود پلتفرم Flag رو وارد Critical Path کرده‌ایم. SDKهای Server-side معمولاً Ruleset رو توی حافظه Cache می‌کنن و تغییرات رو از طریق Streaming یا Polling دریافت می‌کنن. توی مسیرهای کم‌ترافیک این هزینه بی‌اهمیته، اما توی مسیری مثل Checkout یه فروشگاه بزرگ که ممکنه هزاران بار در ثانیه فراخوانی بشه، تفاوت بین دو نوع معماری، کاملاً محسوس می‌شه:

Polling در برابر Streaming: اگه SDK هر بار برای گرفتن مقدار Flag یک Network Call به سرویس بیرونی بزنه، تأخیر و ریسک Timeout مستقیماً وارد مسیر بحرانی برنامه می‌شه. اکثر پلتفرم‌های جدی (LaunchDarkly، Unleash) به همین دلیل یک مدل Streaming یا Polling دوره‌ای دارن: مقادیر Flag یک‌بار توی پس‌زمینه، Cache می‌شن و ارزیابی واقعی صرفاً یک Lookup محلی و در حد میکروثانیه است (cache توی مموری است عموماً).

// ارزیابی محلی، نه یک Network Call در لحظه درخواست
var isEnabled = featureFlagCache.Evaluate("new-checkout", userContext);

رفتار در زمان قطعی: اگه Cache محلی خالی باشه و پلتفرم Flag هم در دسترس نباشه، برنامه چه تصمیمی باید بگیره؟ اینجاست که «رفتار پیش‌فرض و امن» که در بخش Governance بهش اشاره کردم، عملاً تعیین می‌کنه که آیا یک قطعی بیرونی، Checkout رو هم از کار می‌اندازه یا نه.

اندازه‌گیری بدهی: از شعار تا عدد

گفتن «Flagهای موقت باید حذف بشن»، خوبه و کسی هم منکرش نمی‌شه، ولی بدون معیار قابل اندازه‌گیری، هیچ تیمی متوجه نمی‌شه چقدر از این اصل فاصله گرفته. چند معیار عملی:

  • میانگین سن Flagهای فعال (Flag Age): چند روز از ایجاد هر Flag فعال گذشته؟ اگر میانگین این عدد برای Release Flagها بیشتر از چند هفته باشه، جای نگرانی داره.
  • تعداد Flagهای بدون تغییر در ۹۰ روز اخیر: Flagی که مدتی طولانی نه روشن شده، نه خاموش، و نه حذف، معمولاً یا فراموش شده یا به یه Permission واقعی تبدیل شده و باید به همین شکل بازتعریف شه.
  • نسبت Flagهای فعال که Ticket حذف دارن، به کل Flagهای موقت: این عدد نشون می‌ده آیا قانون «هر Flag موقت باید یک برنامه حذف مشخص هم داشته باشه» واقعاً اجرا می‌شه یا فقط روی کاغذ اومده.

بیشتر پلتفرم‌های مدیریت Flag مثل Unleash، LaunchDarkly، امکان Stale Flag Detection دارن و می‌تونن به‌صورت خودکار Flagهای بدون تغییرِ اخیر رو علامت بزنن؛ ولی همون‌طور که قبلاً گفتم، این ابزار فقط زمانی مفیده که تیم واقعاً به هشدارهاش توجه کنه. Unleash براساس Expected Lifetime وضعیت potentially stale ایجاد می‌کنه؛ از طرف دیگه، LaunchDarkly هم Lifecycle، Deprecation، Archive و Flag Health رو داره.


نام‌گذاری: جزئیاتی که قابلیت جست‌وجو رو می‌سازه

یه Flag با عنوان newCheckout بعد از شش ماه دیگه معنایی نداره: کدوم Checkout؟ چرا «جدید»؟ چه زمانی ساخته شده؟ یک قرارداد نام‌گذاری ساختاریافته این ابهام رو از بین می‌بره:

{type}.{domain}.{description}

مثال:

  • release.checkout.new-payment-flow
  • experiment.pricing.discount-banner-v2
  • killswitch.integrations.disable-fraud-provider

این نوع ساختاردهی، سه فایده مستقیم داره:
۱: از روی اسم میشه نوع Flag رو (بدون باز کردن پلتفرم) تشخیص داد
۲:جست‌وجو و فیلتر کردن Flagهای یک Domain خاص ساده می‌شه
۳: و مهم‌تر از همه، ابزارهای Stale Flag Detection می‌تونن بر اساس Prefix، سیاست‌های متفاوتی اعمال کنن؛ (مثلاً هر Flagی با پیشوند experiment که بیش از ۳۰ روز بدون تغییر مونده، به‌طور خودکار Flag بشه).

جمع‌بندی

Feature Flag ابزار بسیار ارزشمندیه. انتشار نرم‌افزار رو امن‌تر می‌کنه، Deployment رو از Release جدا می‌کنه و امکان واکنش سریع به خرابی رو مهیا می‌کنه.

ولی همین if ساده، اگه بدون نوع، مالک و پایان مشخص ساخته بشه، می‌تونه سال‌ها توی کد باقی بمونه. نتیجه: مسیرهای اجرایی ناشناخته، تست‌های پیچیده، کدهای منسوخ و ترس از تغییر می‌شه.

Flagهای موقتی باید حذف بشن. Flagهای دائمی باید آگاهانه به‌عنوان کنترل عملیاتی طراحی بشن. Permission، Entitlement، Configuration و Business Policy هم نباید صرفاً به دلیل راحتی، Feature Flag نامیده بشن.

و در نهایت:

Feature Flag بدهی فنی نیست؛ اما Feature Flag بدون مالک، تاریخ انقضا و برنامه حذف، بدهی فنی زمان‌بندی‌شده است.

۲ دیدگاه دربارهٔ «Feature Flag؛ از یک if ساده‌، تا چیزی که کسی جرئت حذفش رو نداره!;

  1. توقع اینکه مطلب اینقدر مفصل باشه رو نداشتم.دست مریزاد.

    اونجایی که اشاره شده ask ها به صورت محلی cache میکنند،
    سوالی که هست اینه که در چند pod از یک سرویس، مشکل ناهمگام بودن پیش نمیاد؟

    Reply
    • ممنونم، خوشحالم که براتون مفید بوده!

      چون هر pod نسخه‌ای از تنظیمات Flagها رو به‌صورت محلی نگه می‌داره، موقع تغییر یه flag تا رسیدن update به همه podها، ممکنه برای مدت کوتاهی تصمیم‌های متفاوتی گرفته بشه. این همون eventual consistency ه؛ حالا توی حالت streaming معمولا این فاصله خیلی کوتاهه ولی توی polling به فاصله زمانی poll بستگی داره.

      برای یه rollout عادی اغلب قابل قبوله، ولی اگه یه درخواست یا فرایند باید از اول تا آخر، تصمیم یکسانی ببینه، بهتره flag فقط یک‌بار توی مرز مناسب evaluate بشه و نتیجه، ترجیحا همراه نسخه تنظیمات، توی context داخلی یا event منتقل بشه؛ نه اینکه هر سرویس دوباره مستقلا اون رو ارزیابی کنه. توی فرایندهای طولانی هم بهتره تصمیم گرفته‌شده ذخیره بشه.

      برای kill switchهای حساس هم باید این تاخیر انتشار رو جزئی از طراحی در نظر گرفت: مانیتور کردن تازگی cache، یا داشتن مقدار پیش‌فرض امن و آماده نشدن pod قبل از سینک اولیه.

      پس local cache سرعت و تاب‌آوری خوبی می‌ده، ولی هزینه‌اش پذیرفتن و مدیریت همین ناهمگامی کوتاه‌مدته.

      Reply

دیدگاهتان را بنویسید