Feature Flags in Laravel with Pennant

Categories: Laravel Startups
7 min read

Deploy != release. That distinction is small but it changes how you ship. Feature flags let you deploy code to production before it's visible to users - so you can test in the real environment, roll out gradually, and kill a bad feature without a rollback. For Laravel apps, the package to know is Pennant: Laravel's own feature flag solution, built into the ecosystem. Here's how feature flags work, when Pennant fits, and how to avoid the mess that comes from doing it badly.

The Problem Feature Flags Solve

Most teams start with a simple model: code ships when it's ready, and "ready" means deployed to production. That works fine until you have a feature that takes two weeks to build, a team of more than one, or a deploy that needs to happen on a specific date. At that point the old model starts to fight you.

The core problem is that deploy and release are treated as the same event. They don't have to be. You can deploy code with a feature flag wrapping the new behaviour, and release it - make it visible - whenever you choose. The code is in production. It's not active. That gap is where feature flags live.

That buys you a few things:

  • Gradual rollouts. Enable a feature for 5% of users, watch the metrics, expand if it looks good. No big-bang release where everything either works or it doesn't.
  • Kill switches. If something goes wrong, flip the flag off. No rollback, no hotfix deployment, no 2am scramble.
  • Environment-specific features. New behaviour in staging, old behaviour in production, until you're ready to flip it.
  • Beta access. Enable a feature for specific users, a specific team, or paying customers only.
  • A/B testing. Show two variants, measure which performs better, make a decision based on data.

For startups - which is most of what I work on as a freelance Laravel developer - the kill switch and gradual rollout cases are the most valuable. The worst moments I've seen aren't when code is wrong; they're when wrong code is live and the only fix is a deployment under pressure.

Laravel Pennant

Laravel Pennant is Laravel's official feature flag package. It's been part of the ecosystem since Laravel 10 and it fits naturally alongside the rest of the framework: Eloquent scopes, service providers, artisan commands, the works.

You install it with composer require laravel/pennant and publish the migration. Then you define features in a service provider:

Feature::define('new-billing-flow', function (User $user) {
    return $user->isOnBetaPlan();
});

And check them wherever you need to:

if (Feature::active('new-billing-flow')) {
    // show new billing flow
}

Or in Blade:

@feature('new-billing-flow')
    <x-new-billing-flow />
@endfeature

Pennant stores flag state in the database by default (or in an array driver for testing). It resolves features per-scope - usually per user, but you can scope to teams, organisations, or anything else. Once a feature is resolved for a given scope, the result is cached for the request. No N+1 on flag checks.

Percentage-based rollouts

Pennant has a lottery helper for percentage rollouts:

Feature::define('redesigned-dashboard', function (User $user) {
    return Lottery::odds(1, 10); // 10% of users
});

The result is stored, so a user who's in the 10% stays in the 10% across requests. They don't randomly flip in and out.

Artisan commands

Pennant ships with artisan commands to activate or deactivate features from the CLI:

php artisan pennant:activate new-billing-flow
php artisan pennant:deactivate new-billing-flow

That's your kill switch. One command, no deployment.

Where Pennant fits

Pennant is a good fit when you're already in the Laravel ecosystem and your flag requirements are straightforward: per-user flags, percentage rollouts, environment toggles. It's well-maintained, well-documented, and zero extra infrastructure. If you're building a Laravel app with startup discipline, Pennant is the obvious first choice.

Where Pennant is less suited: multi-language services (it's PHP-only), real-time flag updates without a cache flush, or advanced targeting rules that need a UI your product team can operate without touching code or the database.

Alternatives

Dedicated feature-flag platforms exist for when you outgrow Pennant: LaunchDarkly, GrowthBook, and others. They're worth a look once you've got multiple services across different stacks, non-technical stakeholders who need to manage flags through a UI, or experimentation needs that go beyond on/off. For a single Laravel app, Pennant is fine.

Pragmatic Patterns

A few patterns that save headaches:

One flag per thing. Don't reuse a flag across unrelated features because the names overlap. Flag "new-onboarding" means new onboarding - not "anything we changed in April". Flags should describe what they control, not when they were added.

Don't flag everything. Not every change needs a flag. Small fixes, internal refactors, and changes that go out in a normal release don't need the overhead. Add flags when the risk of the change justifies the extra complexity. A new checkout flow? Yes. A CSS tweak? No.

Test both paths. If a flag has two states, your tests should cover both. A feature that only gets tested with the flag on will silently break when the flag is off. In Pennant, the array driver makes this simple in tests:

Feature::activate('new-billing-flow');
// test with flag on

Feature::deactivate('new-billing-flow');
// test with flag off

Feature flags and MVP scope go together. If you're building a minimal first version, flags let you ship the skeleton and progressively enable parts of it as they're ready - without maintaining long-lived feature branches. That's cleaner than sitting on a branch for three weeks.

Flag Debt Is Real

The most common mistake with feature flags is accumulating them and never cleaning up. A flag that was meant to guard a rollout six months ago, now permanently active, is now just dead code with extra steps. The flag check is still in every request. The "inactive" branch is still in the codebase. Nobody knows if it's safe to remove.

Flag debt compounds. Once you have ten permanent flags and nobody remembers what they guard, adding new flags feels risky and removing old ones feels risky. The codebase becomes harder to read and harder to change.

The fix is simple but requires discipline: treat removing a flag as part of completing the feature. When a rollout is complete and the flag is fully active, schedule the cleanup. Delete the flag definition, remove the checks, remove the dead code path. It's a boring task, which is why it gets skipped. Don't skip it.

Pennant's php artisan pennant:purge removes stored flag values from the database for flags that no longer exist in code. Run it after cleanup as part of your deployment.

Getting Started

If you're not using feature flags at all, start small. Pick one upcoming change that carries some risk - a reworked onboarding flow, a new payment method, a redesigned part of the UI. Wrap it in a Pennant flag. Deploy it off. Turn it on for yourself and your team. Expand from there.

You don't need a full feature flag strategy from day one. You need just enough flags for the next risky thing you're about to ship. That's the right scope. Over-engineering the flag system at the start is the same mistake as over-engineering anything else - you'll build infrastructure for problems you don't have yet.

For more on keeping a Laravel startup codebase manageable as it grows, see Laravel best practices for startups and scaling your Laravel backend. And if you're starting from scratch, building a minimal MVP covers how to scope the first version without painting yourself into a corner.

I'm a freelance Laravel developer based in Wiltshire, near Bath, working with startups and growing teams across Bath, Bristol, and the UK. If you're working out how to ship safely at pace - whether that's feature flags, deployment strategy, or just making the codebase easier to change - reach out.

Ben Lumley StackOverflow Github Linkedin

Related posts