Feature Flags: Write Your Own or Pay for LaunchDarkly?
I've rolled my own feature flags a dozen times and used LaunchDarkly on two projects. Here's the honest inflection point between them.
I've built feature flag systems from scratch more times than I can count, and I've also wired up LaunchDarkly on two production projects. The honest answer to "which should I use" is not the one most blog posts give you. Most posts either sell you on LaunchDarkly immediately or tell you it's overkill. Both are sometimes wrong.
Here's the actual inflection point, with working code on both sides.
What Feature Flags Actually Solve
The core problem is simple: you want to deploy code to production without turning it on for everyone. That's it. Everything else — percentage rollouts, user targeting, kill switches, A/B testing — is a variation on that theme.
The second problem, which people underestimate, is who needs to control the flag. If it's only engineers toggling a config value between deploys, that's a different tool than a product manager needing to flip a feature for a specific customer segment at 2pm on a Friday without touching code.
That distinction is where the build-vs-buy decision actually lives.
The 50-Line Laravel Implementation
Here's what I drop into projects that need basic feature flags. This has covered 90% of my use cases for the last several years.
First, a migration:
Schema::create('feature_flags', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->boolean('enabled')->default(false);
$table->json('metadata')->nullable(); // percentage, user_ids, etc.
$table->timestamps();
});
Then the model and a service class:
<?php
namespace App\Services;
use App\Models\FeatureFlag;
use Illuminate\Support\Facades\Cache;
class FeatureFlagService
{
public function isEnabled(string $key, ?int $userId = null): bool
{
$flag = Cache::remember(
"feature_flag:{$key}",
now()->addMinutes(5),
fn () => FeatureFlag::where('key', $key)->first()
);
if (! $flag || ! $flag->enabled) {
return false;
}
$meta = $flag->metadata ?? [];
// Allowlist of specific user IDs
if (! empty($meta['user_ids']) && $userId !== null) {
return in_array($userId, $meta['user_ids']);
}
// Percentage rollout — deterministic per user
if (isset($meta['percentage']) && $userId !== null) {
return (crc32($key . $userId) % 100) < $meta['percentage'];
}
return true;
}
}
Wire it up as a singleton in a service provider, then use it:
// In AppServiceProvider::register()
$this->app->singleton(FeatureFlagService::class);
// Anywhere in your app
$flags = app(FeatureFlagService::class);
if ($flags->isEnabled('new_checkout_flow', auth()->id())) {
return view('checkout.v2');
}
return view('checkout.v1');
And a Blade directive if you want clean templates:
// In AppServiceProvider::boot()
Blade::if('feature', function (string $key) {
return app(FeatureFlagService::class)->isEnabled($key, auth()->id());
});
@feature('new_dashboard')
<x-dashboard-v2 />
@else
<x-dashboard-v1 />
@endfeature
That's the whole thing. Database-backed, cached, supports user allowlists, supports percentage rollouts with deterministic bucketing (same user always gets the same experience — critical for checkout flows). You get a Nova resource or a quick Filament panel on top and a non-technical person can toggle flags too.
I've shipped this pattern into healthcare portals, e-commerce sites, and a print management platform. It handles a lot.
The Gotchas in Rolling Your Own
A few things will bite you if you're not careful.
The cache invalidation problem. Five-minute TTL means a flag change takes up to five minutes to propagate. For a kill switch on a broken feature, that's a long five minutes. I've moved time-sensitive flags to a 30-second TTL and added a manual cache-bust endpoint behind an admin route. Not elegant, but it works.
Deterministic bucketing breaks on key changes. The crc32($key . $userId) trick means if you rename a flag key, users get re-bucketed. Never rename a flag that's mid-rollout. Delete it when you're done and create a new one.
You have no audit trail out of the box. LaunchDarkly logs every flag change with who did it and when. Your homegrown system logs nothing unless you add it. For healthcare clients specifically, I always add a flag_audit_log table and an Eloquent observer. You do not want to be debugging "who turned this off on Sunday" without a log.
Multi-environment management gets annoying. One database per environment is fine. But if you have staging, UAT, and production and you want flag parity across them, you're hand-managing that. There's no sync mechanism. It's a spreadsheet problem waiting to happen.
Where LaunchDarkly Actually Earns Its Price
LaunchDarkly starts around $10-12 per seat per month for the base tier, and it goes up fast for enterprise features. I'm not going to pretend that's nothing for a small shop.
But here's where I've found it genuinely worth it:
When non-engineers own the flags. I integrated LaunchDarkly for a mid-sized SaaS client whose product managers were releasing features on a rolling schedule to different customer tiers. The PMs needed to control rollouts themselves — during business hours, without filing a ticket, without touching a database. LaunchDarkly's UI is actually good. Non-engineers can use it without hand-holding.
When you have more than one service. My homegrown system is database-backed, which means it's Laravel-specific. If you've got a Python service, a Node microservice, and a Laravel API all needing the same flags, you either build a flag API yourself or you use something that already has SDKs for all three. LaunchDarkly has SDKs for everything. The PHP SDK is solid:
use LaunchDarkly\LDClient;
$client = new LDClient(env('LAUNCHDARKLY_SDK_KEY'));
$context = \LaunchDarkly\LDContext::create((string) $user->id);
if ($client->variation('new-checkout-flow', $context, false)) {
return view('checkout.v2');
}
return view('checkout.v1');
The SDK handles streaming updates, so flag changes propagate in milliseconds — not five minutes. For a kill switch, that matters.
When you need real targeting rules. LaunchDarkly lets you target on arbitrary user attributes — plan type, signup date, company size, geographic region. My 50-line version does user IDs and percentages. That's often enough. But the moment a client asks "can we enable this only for users on the Pro plan who signed up after March?" you're either writing a rules engine or you're buying one.
When compliance requires an audit trail you didn't build. LaunchDarkly keeps a full change history. For one biotech client I work with, having that out of the box saved a conversation about who changed what before a release.
When I'd Reach for Each
Roll your own when:
- It's a single Laravel app
- Engineers are the primary flag operators
- You need simple on/off or basic percentage rollouts
- You're a small team or a startup watching burn rate
- The app is early-stage and you'd rather not add vendor dependencies
Pay for LaunchDarkly when:
- Non-engineers need to control flags independently
- You're running multiple services in different languages
- You need sub-second flag propagation for kill switches
- Your targeting rules are getting complicated
- You have compliance or audit requirements you don't want to build yourself
- You're operating at enough scale that a 5-minute stale flag could cost you real money
The honest inflection point is somewhere around the moment a product manager sends you a Slack message asking you to flip a flag. If that happens once a month, build your own and add a simple admin UI. If it happens daily, or if the answer to "who should be able to do this" is "not just engineers," stop building and start paying.
The Closing Take
I default to the 50-line version. It's shipped in probably fifteen projects and it has never been the bottleneck. But I don't have any ideological attachment to it — when a client's workflow outgrows it, LaunchDarkly is a legitimate product that solves a real problem. The mistake is reaching for it on day one of a solo Laravel project because someone wrote a blog post about feature flag best practices. Start simple, and let the actual pain tell you when to upgrade.
Need help shipping something like this? Get in touch.