Feature Flags for Small Teams: No Platform Required

Feature flags are not just for big tech. How to add simple, safe flags to a Laravel or Go app with a config file or a database table — gradual rollouts, kill switches, and the discipline that keeps them working.

Feature Flags for Small Teams: No Platform Required

Feature flags sound like enterprise machinery: dashboards, SDKs, percentage rollouts managed from a web UI. Most teams never adopt them for that reason — and then keep deploying incomplete features or rolling back whole releases to disable one broken thing.

You do not need a platform. A flag is just an if. What matters is where the if lives and how quickly you can flip it.

What flags actually buy you

  • Deploy decoupled from release. Merge code to master while it's hidden behind a flag. The deploy happens on your schedule; the feature goes live when it's ready.
  • Instant kill switch. If a new checkout flow misbehaves in production, you disable it in seconds instead of reverting a release at midnight.
  • Gradual exposure. Show the feature to internal users, then a percentage of everyone, then all. Real traffic finds bugs your test suite missed.
  • Cheap experiments. Try two label texts, two pricing displays — without two branches or two deploys.

Level 1: a config file

For most small products this is enough. In Laravel, a simple class over config():

// app/Support/Features.php
class Features
{
    public static function enabled(string $name, $context = null): bool
    {
        $flag = config("features.{$name}");
        if (is_array($flag)) {
            return in_array($context?->email ?? null, $flag['users'] ?? []);
        }
        return (bool) $flag;
    }
}
// config/features.php
return [
    'new-invoice-editor' => false,
    'beta-dashboard' => ['users' => ['max@example.com']],
];

At the call site:

@if (Features::enabled('new-invoice-editor', auth()->user()))
    <new-invoice-editor />
@else
    <old-invoice-editor />
@endif

Flipping a flag means editing one file and deploying — or, if the config lives in the database, flipping it with one artisan tinker command. Either way, no rollback and no merge.

Level 2: a database table

When you want to toggle without a deploy, add a tiny table: name, enabled, rollout_percent, updated_at. Read it with caching (60 seconds is fine) so a flag check never becomes a query-per-request problem:

$flags = Cache::remember('feature-flags', 60,
    fn () => FeatureFlag::query()->pluck('enabled', 'name')->all());

Percentage rollout can be deterministic rather than random — hash the user ID, take the modulo, compare against the rollout percent. The same user sees the same experience on every request, which matters more than statistical purity.

The rules that keep flags from rotting

Flags fail not in the code but in the hygiene. Three rules we hold to:

  1. Every flag has an owner and a purpose. Name them descriptively (new-checkout-v2, not flag3) and note in the PR what removing it will take.
  2. Remove dead flags aggressively. A flag that's been fully rolled out for two weeks is a cleanup task, not a permanent resident. Flag debt compounds: after a year of "we'll clean up later," every code path doubles.
  3. Test both branches. A feature behind a flag has two states in production. If your test suite only covers the enabled path, the flag is hiding your untested code, not protecting it.

What not to do

  • Don't wrap every function in flags. Flags are for release control and experiments, not for code structure. If everything can be toggled, nothing can be reasoned about.
  • Don't put flag logic deep in the domain layer. Check flags at the edge — controllers, route registration, UI composition — and keep the core logic flag-free.
  • Don't skip the "off" state design. A feature that crashes when disabled wasn't behind a flag; it was behind a hope.

The payoff

The pattern costs an afternoon to set up and pays for itself the first time a release goes sideways. For a two-person team shipping weekly, that's not enterprise overhead — it's the difference between "roll back now" and "flip one boolean."

The same idea translates directly to Go services — a map[string]bool loaded from a config or an environment, checked at the handler boundary — and to any stack where a deploy takes longer than a config change. Start with one flag on your next risky feature. You'll add the second one yourself.