DEV Community

Cover image for Stop Branch Wars: Feature Flags in Laravel
Prajapati Paresh
Prajapati Paresh

Posted on • Originally published at smarttechdevs.in

Stop Branch Wars: Feature Flags in Laravel

The Merge Conflict Nightmare

As engineering teams scale at Smart Tech Devs, the standard "Git Flow" methodology becomes a massive bottleneck. A developer creates a feature/new-billing branch and works on it for three weeks. Meanwhile, the rest of the team merges 50 other pull requests into the main branch.

When the billing developer finally tries to merge their three-week-old branch, they are hit with a catastrophic wall of merge conflicts. The codebase has drifted too far. Days are wasted resolving conflicts, and production bugs inevitably slip through the cracks. To achieve true CI/CD and deploy multiple times a day, you must abandon long-lived feature branches and adopt Trunk-Based Development paired with Feature Flags.

The Solution: Trunk-Based Development

In Trunk-Based Development, developers merge their code directly into the main branch every single dayโ€”even if the feature is only 20% finished. To prevent the unfinished feature from breaking the live production site, you wrap the code in a Feature Flag.

The code is safely sitting in production, but it is mathematically invisible to the end user until you flip the switch. In Laravel, this architecture is natively powered by an elegant first-party package called Laravel Pennant.

Architecting Feature Flags with Pennant

First, we define our feature flag in the AppServiceProvider. We can configure it to be completely disabled, or selectively enabled for specific users (like internal QA testers or premium beta users).


// app/Providers/AppServiceProvider.php
namespace App\Providers;

use App\Models\User;
use Laravel\Pennant\Feature;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // โœ… THE ENTERPRISE PATTERN: Selective Rollouts
        // The new billing engine is ONLY active for employees and Beta testers.
        // For everyone else, it remains safely dormant in production.
        Feature::define('new-billing-engine', function (User $user) {
            return $user->is_employee || $user->in_beta_program;
        });
    }
}

Executing the Flagged Logic

In our controllers or background jobs, we check the flag before executing the new logic. If the flag is false, the application gracefully falls back to the legacy code.


// app/Http/Controllers/BillingController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Laravel\Pennant\Feature;

class BillingController extends Controller
{
    public function process(Request $request)
    {
        // The branching happens at runtime, not in Git!
        if (Feature::active('new-billing-engine')) {
            return $this->processViaStripeV2($request);
        }

        return $this->processViaLegacySystem($request);
    }
}

The Engineering ROI

By migrating to Trunk-Based Development and Laravel Pennant, you completely eradicate "merge hell." Your developers merge code daily, keeping the repository perfectly in sync. You gain the superpower of decoupling deployment (pushing code to the server) from release (showing the feature to the user), allowing you to safely test features in production and instantly roll them back with a single database toggle if something goes wrong.

Top comments (0)