DEV Community

Cover image for Decoupling Deployment: Feature Flags in Laravel 🚩
Prajapati Paresh
Prajapati Paresh

Posted on • Originally published at smarttechdevs.in

Decoupling Deployment: Feature Flags in Laravel 🚩

The Nightmare of Long-Lived Feature Branches

In traditional software development lifecycles, engineering teams often rely on long-lived feature branches. A developer might branch off from main, spend three weeks building a massive new billing engine, and then attempt to merge it back. By that time, main has progressed significantly. The resulting merge conflicts are catastrophic, requiring days of manual resolution and introducing a severe risk of regression bugs.

Furthermore, deploying that code means simultaneously releasing it to all users. If there is a critical flaw in the new billing engine, the entire platform goes down, requiring an emergency rollback of the entire deployment. This couples Deployment (pushing code to a server) tightly with Release (exposing a feature to users).

At Smart Tech Devs, we decouple deployment from release by practicing Trunk-Based Development powered by Feature Flags (also known as Feature Toggles). We merge incomplete code into the main branch multiple times a day, deploying it straight to production, but we hide that code behind a cryptographic or database-driven flag. The code is physically on the server, but completely invisible to the user.

Enter Laravel Pennant

While you can build a rudimentary feature flag system using a is_active boolean on your users table, enterprise applications require complex rollout strategies (e.g., "Only enable this feature for internal employees," or "Roll this out to 10% of our premium users"). To architect this, we utilize Laravel Pennant, a robust, first-party feature flag package that integrates perfectly with Laravel's Redis cache and Eloquent models.

Phase 1: Defining the Features (Class-Based)

Instead of scattering magical string names like 'new-billing-v2' throughout our codebase, we define our features as strict, typed classes. This provides IDE auto-completion and keeps our rollout logic highly centralized and testable.


namespace App\Features;

use App\Models\User;
use Illuminate\Support\Lottery;

class NewBillingEngine
{
    /**
     * Resolve the feature's initial value.
     * This logic determines WHO gets the feature when they first interact with it.
     */
    public function resolve(User $user): mixed
    {
        // 1. Always enable for internal company testing
        if ($user->email_ends_with === '@smarttechdevs.in') {
            return true;
        }

        // 2. Do not enable for users with unpaid invoices
        if ($user->has_overdue_invoices) {
            return false;
        }

        // 3. Gradual Rollout: Enable for a random 20% of eligible users
        // Laravel's Lottery class makes statistical rollouts incredibly simple
        return Lottery::odds(2, 10)->choose();
    }
}

Phase 2: Architectural Implementation

With our feature defined, we can confidently push the new billing engine code to production. We wrap our new logic inside a conditional block checking the feature flag. If the flag is false, the application gracefully falls back to the old, stable legacy code.

Because Pennant stores the resolved feature state in the database or Redis, a user who hits the 20% lottery will consistently see the new feature on subsequent requests, while the other 80% will remain completely unaffected.


namespace App\Http\Controllers;

use App\Features\NewBillingEngine;
use Illuminate\Http\Request;
use Laravel\Pennant\Feature;

class SubscriptionController extends Controller
{
    public function process(Request $request)
    {
        // The Feature facade automatically injects the currently authenticated user
        if (Feature::active(NewBillingEngine::class)) {
            
            // Execute the brand new, experimental V2 Billing Logic
            return $this->processWithStripeV2($request);
            
        }

        // Fallback to the rock-solid V1 Billing Logic
        return $this->processLegacyBilling($request);
    }
}

Phase 3: The Frontend and API Perimeters

Feature flags must permeate your entire stack. It is not enough to conditionally execute backend logic; you must also conditionally render the UI elements (like a "Try our new Billing Dashboard" button). Pennant provides seamless Blade directives for monolithic applications, and clean APIs for headless architectures.

For a standard Laravel Blade view, the implementation is elegant:


<!-- resources/views/dashboard.blade.php -->
<nav>
    <a href="/home">Home</a>
    
    @feature(\App\Features\NewBillingEngine::class)
        <a href="/billing-v2" class="text-neon-blue">New Billing Portal (Beta)</a>
    @else
        <a href="/billing">Billing</a>
    @endfeature
</nav>

If you are building an API for a React or Next.js frontend, you can easily expose a user's active flags via a dedicated endpoint or inject them directly into your initial Inertia.js payload:


Route::get('/api/user/flags', function (Request $request) {
    return response()->json([
        'flags' => Feature::all()
    ]);
});

Advanced Operations: The Kill Switch

The ultimate architectural benefit of Feature Flags is the Instant Kill Switch. Imagine your 20% rollout of the NewBillingEngine is causing database deadlocks in production. In a traditional architecture, resolving this requires a developer to revert the Git commit, wait 15 minutes for the CI/CD pipeline to build the Docker image, and deploy it to the servers.

With Laravel Pennant, your operations team can instantly disable the feature for all users globally via a single Artisan command or a secure admin dashboard, without writing a single line of code or running a deployment:


# Instantly deactivate the feature for all users, overriding the resolve() logic
php artisan pennant:purge App\Features\NewBillingEngine

The Engineering ROI

Implementing Feature Flags via Laravel Pennant fundamentally transforms an engineering organization. By adopting Trunk-Based Development, you eliminate merge conflicts and accelerate code integration. By decoupling deployment from release, you empower product managers to control rollout schedules independently of engineering deployments. Most importantly, by introducing statistical rollouts and instant kill switches, you drastically reduce the blast radius of production bugs, ensuring your enterprise application maintains maximum uptime and resilience during rapid iteration cycles.

Top comments (0)