The phrase "deploy to production" conflates two distinct actions: putting code on servers, and making features available to users. Progressive delivery is the discipline of separating them — getting code deployed to production continuously while controlling feature exposure through flags, gradual rollouts, and targeted releases.
When done well, you stop having "big bang" releases and start having a continuous stream of small, controlled exposures. Bugs are caught with limited blast radius. Experiments run without additional deployments. Rollback becomes instant.
The Difference Between Deploy and Release
Without feature flags:
Code merged → CI passes → Deploy to production → All users see new feature
With progressive delivery:
Code merged → CI passes → Deploy to production → Feature is off
↓
Gradual rollout: 1% → 10% → 50% → 100%
↓
Full release when metrics look good
The code deployment happens once. The release is a controlled, observable event.
Building a Feature Flag System
You can build a simple but effective feature flag system without a commercial tool:
// app/Services/FeatureFlagService.php
class FeatureFlagService
{
public function __construct(private Cache $cache) {}
public function isEnabled(string $flag, ?User $user = null): bool
{
$config = $this->getFlag($flag);
if (!$config) {
return false; // Unknown flags default to off
}
if ($config['status'] === 'off') {
return false;
}
if ($config['status'] === 'on') {
return true;
}
if ($config['status'] === 'percentage') {
return $this->isInPercentage($config['percentage'], $flag, $user);
}
if ($config['status'] === 'users' && $user) {
return in_array($user->id, $config['user_ids'], true);
}
if ($config['status'] === 'roles' && $user) {
return $user->hasAnyRole($config['roles']);
}
return false;
}
private function isInPercentage(int $percentage, string $flag, ?User $user): bool
{
// Use stable hash so the same user always gets the same result
$identifier = $user ? "user:{$user->id}" : session()->getId();
$hash = crc32($flag . ':' . $identifier);
$bucket = abs($hash) % 100;
return $bucket < $percentage;
}
private function getFlag(string $flag): ?array
{
return $this->cache->remember(
"feature_flags:{$flag}",
300, // Cache for 5 minutes
fn () => FeatureFlag::where('key', $flag)->first()?->toArray()
);
}
}
The database table:
// Migration
Schema::create('feature_flags', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->string('description');
$table->enum('status', ['off', 'on', 'percentage', 'users', 'roles']);
$table->integer('percentage')->nullable();
$table->json('user_ids')->nullable();
$table->json('roles')->nullable();
$table->timestamps();
});
Using Flags in Code
Feature flags should be used at decision points, not sprinkled throughout a feature's internals:
// Good: flag at the entry point
class CheckoutController
{
public function store(CheckoutRequest $request): Response
{
if ($this->flags->isEnabled('enhanced-checkout', $request->user())) {
return $this->enhancedCheckout->process($request);
}
return $this->legacyCheckout->process($request);
}
}
// Avoid: flag buried inside a service — harder to clean up later
class PaymentService
{
public function charge(Order $order): void
{
if ($this->flags->isEnabled('new-payment-flow')) { // Hard to find later
// ...
}
}
}
For frontend, expose flag state via an API endpoint:
class FeatureFlagController
{
public function index(Request $request): JsonResponse
{
$user = $request->user();
// Only expose flags relevant to the frontend
$frontendFlags = [
'enhanced-checkout',
'new-dashboard',
'beta-reporting',
];
$flags = collect($frontendFlags)->mapWithKeys(fn ($flag) => [
$flag => $this->flags->isEnabled($flag, $user),
]);
return response()->json($flags);
}
}
// JavaScript: check flags before rendering
const { data: flags } = await axios.get('/api/feature-flags');
if (flags['enhanced-checkout']) {
renderEnhancedCheckout();
} else {
renderLegacyCheckout();
}
Gradual Rollout Strategy
A typical rollout progression for a significant feature:
Day 1: Internal team only (users: specific user IDs)
Day 3: Beta users and internal (roles: beta)
Day 5: 1% of all users (percentage: 1)
Day 7: Review metrics — error rates, conversion, performance
Day 8: 10% if metrics look good
Day 10: 50%
Day 12: 100% → archive the flag
At each step, you monitor:
// Track which variant users are in for metric segmentation
class RolloutMetricsService
{
public function trackConversion(User $user, string $event): void
{
$variant = $this->flags->isEnabled('enhanced-checkout', $user)
? 'enhanced'
: 'legacy';
Analytics::track([
'user_id' => $user->id,
'event' => $event,
'properties' => [
'checkout_variant' => $variant,
'feature_flag' => 'enhanced-checkout',
],
]);
}
}
This lets you compare conversion rates, error rates, and performance metrics between the control (legacy) and treatment (enhanced) groups.
Integrating With Deployment Pipelines
Feature flags let you deploy continuously from the main branch without needing long-lived feature branches:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: node scripts/deploy/deploy.cjs
# No feature flag changes here — flags are changed independently
# by the product team through the admin UI or API
The deployment always deploys all code. Feature visibility is controlled separately through the flag admin interface.
Kill Switches
Not every flag is a gradual rollout. Kill switches let you instantly disable a problematic feature:
// A kill switch that defaults to ON (feature enabled) and can be turned off
class ApiRateLimitService
{
public function shouldRateLimit(Request $request): bool
{
// Kill switch: disable rate limiting immediately if it's causing issues
if (!$this->flags->isEnabled('api-rate-limiting-enabled')) {
return false;
}
return $this->isOverLimit($request);
}
}
// Protecting an integration with a kill switch
class SlackNotificationService
{
public function send(Notification $notification): void
{
if (!$this->flags->isEnabled('slack-notifications')) {
Log::debug('Slack notifications disabled via feature flag');
return;
}
$this->slack->post($notification);
}
}
When Slack is having an incident and your notifications are failing or slowing down your app, you flip the flag and stop sending immediately.
Commercial Feature Flag Tools
For larger teams, commercial tools add significant capabilities:
LaunchDarkly: Real-time flag evaluation, built-in A/B testing, flag analytics, SDKs for every language. Flags evaluated server-side with sub-millisecond latency via persistent connections.
Unleash: Open-source, self-hostable, with enterprise features. Good choice if you want control over data.
Flagsmith: Open-source with cloud option, supports A/B testing and remote configuration.
AWS AppConfig: Integrated with AWS, supports deployment validators that can auto-roll back when error rates spike.
For a team shipping several features per week, a commercial tool pays for itself quickly in reduced release coordination overhead.
Flag Hygiene: Clean Up After Yourself
The most common problem with feature flags is accumulation. Flags that were supposed to be temporary live for years and become coupling points that nobody wants to remove.
Establish a cleanup process:
// Track when a flag was fully rolled out
Schema::table('feature_flags', function (Blueprint $table) {
$table->timestamp('fully_rolled_out_at')->nullable();
$table->string('ticket_to_clean_up')->nullable(); // e.g., 'JIRA-1234'
});
Add a rule: when a flag reaches 100%, create a cleanup ticket before moving on. Set the fully_rolled_out_at date. If a flag has been at 100% for more than 30 days, it should be removed from the code.
A flag at 100% that is not removed is dead code plus complexity. The branch in your code will never execute. Remove it.
Progressive delivery is a discipline, not just a tool. The real value comes from changing how your team thinks about deploying code and releasing features — as two separate, independently controlled events.
Building secure, reliable systems? We help teams deliver software they can trust. scopeforged.com