James
Posted on March 12, 2020
Note: This article was originally posted on devsushi.co.
Feature Flags (sometimes known as Feature Toggles) are a useful way of changing the behaviour of a system without modifying the code itself.
Feature Toggles (often also referred to as Feature Flags) are a powerful technique, allowing teams to modify system behaviour without changing code.
Since discovering their benefits, I've been using feature flags, albeit sparingly, in my applications for a number of reasons.
Integrate incomplete features ā if I'm working on a feature that isn't quite ready, but I don't want to maintain a feature branch for an extended period of time, feature flags allow me to integrate an incomplete feature.
Rollback of system functionality ā Feature flags provide an extra safeguard against problems with a feature. If something unexpected happens, I can simply disable the feature without worrying about deploying a fix right away.
Schedule the release of a feature ā If I want to schedule the release of a feature, usually I'd have to schedule an entire deploy at the time I want the feature to be made available. With feature flags, however, I can deploy with the feature "off" in advance and just schedule the triggering of it.
A/B testing ā Since feature flags are a simple boolean on/off, they can be used to build a very simple A/B testing framework within an application.
Building a Feature Flag Manager
Note: This article uses Laravel 7 to implement feature flags, but they can be applied to any version.
So how can we incorporate the benefits of feature flags into our applications? When building any new functionality, I like to see how I'll be interacting with it as a developer; how might we want to use our new "feature manager"?
// Retrieve the feature manager from the container
$manager = $app->get(FeatureManager::class);
// Request the feature
$feature = $manager->feature('my_feature');
// Check the status of the feature and execute code accordingly
if ($feature->enabled()) {
// The 'my_feature' feature is enabled
} else {
// The 'my_feature' feature is disabled
}
This is a simple but effective interface ā we can quickly check the status of a feature and perform a different task depending on whether it's enabled or disabled.
Implementation
Now that we know how we want to use our feature manager, we can begin implementing it. Let's begin with a representation of a "feature":
<?php declare(strict_types=1);
namespace App\Features;
class Feature
{
private bool $enabled;
public function __construct(bool $enabled)
{
$this->enabled = $enabled;
}
public function enabled(): bool
{
return $this->enabled;
}
}
This class will allow us to see whether a feature we've asked for is enabled, and will be returned when we request a feature from our manager later on.
Next we'll implement the mechanism that will actually retrieve the boolean value for a requested feature. This value could be stored in a number of different places, such as a native PHP array, a Redis cluster or a relational database. With this in mind, we'll first add an interface to support multiple implementations:
<?php declare(strict_types=1);
namespace App\Features\Adapters;
interface FeatureAdapter
{
public function feature(string $name, bool $default = false): bool;
}
Our FeatureAdapter
interface will act as a contract for our different adapter implementations. It also allows us to specify a default value if we can't find the flag requested. For our first implementation, let's create a native PHP array adapter:
<?php declare(strict_types=1);
namespace App\Features\Adapters;
class ArrayFeatureAdapter implements FeatureAdapter
{
private array $features;
public function __construct(array $features)
{
$this->features = $features;
}
public function feature(string $name, bool $default = false): bool
{
if (!isset($this->features[$name])) {
return $default;
}
return $this->features[$name];
}
}
Our ArrayFeatureAdapter
takes an associative array of features as its sole argument. The feature
method is petty self-explanatory; it will search the array by key for the feature name and return the associated value if found.
Now that we've created our first adapter, it's time to add the manager itself. This will take in an instance of the adapter we want to use as it's only constructor argument:
<?php declare(strict_types=1);
namespace App\Features;
use App\Features\Adapters\FeatureAdapter;
class FeatureManager
{
private FeatureAdapter $adapter;
public function __construct(FeatureAdapter $adapter)
{
$this->adapter = $adapter;
}
public function feature(string $name, bool $default = false): Feature
{
return new Feature($this->adapter->feature($name, $default));
}
}
The manager simply forwards our feature request to the adapter itself before wrapping it in the Feature
class we created at the beginning. This helps abstract the inner workings of our adapters from the rest of our application by maintaining a consistent interface.
Putting It All Together
Before we can use our FeatureManager
, we'll need to bind it to our container with our chosen adapter:
<?php declare(strict_types=1);
namespace App\Providers;
use App\Features\Adapters\ArrayFeatureAdapter;
use App\Features\Adapters\FeatureAdapter;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(FeatureAdapter::class, static function () {
return new ArrayFeatureAdapter(config('features'));
});
}
}
Here's what our feature config (config/features.php
) looks like:
<?php declare(strict_types=1);
return [
'feature_one' => true,
'feature_two' => false,
];
We've defined two features; feature_one
which is enabled, and feature_two
which is disabled.
We can test it out in a controller like so:
<?php declare(strict_types=1);
namespace App\Http\Controllers;
use App\Features\FeatureManager;
use Illuminate\Contracts\Support\Renderable;
class IndexController extends Controller
{
private FeatureManager $manager;
public function __construct(FeatureManager $manager)
{
$this->manager = $manager;
}
public function index(): Renderable
{
$featureOne = $this->manager->feature('feature_one');
$featureTwo = $this->manager->feature('feature_two');
dump($featureOne->enabled()); // true
dd($featureTwo->enabled()); // false
}
}
Bonus: Additional Adapters
The ArrayFeatureAdapter
we created earlier is all well and good, but we'd still need to deploy code to update a feature flag ā something that we're trying to avoid. Below are a couple of examples that would allow us to manage flags "remotely". Better still we could eventually build a UI around them to make toggling features even easier.
Eloquent Adapter
An obvious choice for feature flag storage is a database table. Since we're using Laravel here, we'll use Eloquent to access them. We'll start with a migration to create our features
database table.
<?php declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFeaturesTable extends Migration
{
public function up(): void
{
Schema::create('features', static function (Blueprint $table) {
$table->id();
$table->string('name');
$table->boolean('is_enabled');
$table->unique('name');
$table->index('name');
});
}
public function down(): void
{
Schema::dropIfExists('features');
}
}
Next, we'll create the accompanying Feature
model for the features
table.
<?php declare(strict_types=1);
namespace App;
use Illuminate\Database\Eloquent\Model;
class Feature extends Model
{
protected $casts = [
'is_enabled' => 'bool',
];
}
Our model will automatically cast our flag values to a boolean for us.
Finally comes the adapter itself. This is straight forward too:
<?php declare(strict_types=1);
namespace App\Features\Adapters;
use App\Feature;
class EloquentFeatureAdapter implements FeatureAdapter
{
public function feature(string $name, bool $default = false): bool
{
$feature = Feature::where('name', $name)->first();
if (null === $feature) {
return $default;
}
return $feature->is_enabled;
}
}
Here we find the feature by its name. If we can't find it, we merely return the default value. All that needs to change in order to use this adapter is our service provider:
<?php declare(strict_types=1);
namespace App\Providers;
use App\Features\Adapters\EloquentFeatureAdapter;
use App\Features\Adapters\FeatureAdapter;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(FeatureAdapter::class, EloquentFeatureAdapter::class);
}
}
Redis Adapter
A Redis adapter would pretty much follow the same approach as the other adapters we've created. First we'll need to install the predis/predis
package:
composer require predis/predis
Here's the adapter itself:
<?php declare(strict_types=1);
namespace App\Features\Adapters;
use Illuminate\Support\Facades\Redis;
class RedisFeatureAdapter implements FeatureAdapter
{
public function feature(string $name, bool $default = false): bool
{
$feature = Redis::get($name);
if (null === $feature) {
return $default;
}
return (bool) $feature;
}
}
Much like the Eloquent adapter, we request the value and, if it exists, return it. Again, we just need to update our container binding to use it:
<?php declare(strict_types=1);
namespace App\Providers;
use App\Features\Adapters\FeatureAdapter;
use App\Features\Adapters\RedisFeatureAdapter;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(FeatureAdapter::class, RedisFeatureAdapter::class);
}
}
That's It!
We've outlined the benefits of using feature flags and I've shown how they can be implemented in a Laravel application. Of course they can be easily adapted to any application ā you'll just need to change how you register the feature manager with your container.
I'd advise against using feature flags absolutely everywhere, but when used sparingly they can certainly provide freedom and flexibility in how we develop and deploy features.
Thanks for reading! Don't forget, you can get all of the code you've seen here on GitHub. If you enjoyed the article, consider sharing it or giving me a follow on Twitter, and stay tuned for more āļø
Posted on March 12, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.