Flippant

Fast feature toggling for Elixir applications, backed by Redis.

Installation

If available in Hex, the package can be installed as:

  1. Add flippant and redix to your list of dependencies in mix.exs:

        def deps do
          [{:flippant, "~> 0.1"},
           {:redix, "~> 0.4"}]
        end
        ```
  2. Ensure flippant and redix is started before your application:

        def application do
          [applications: [:redix, :flippant]]
        end
        ```
  3. Set an adapter within your config.exs:

        config :flippant,
               adapter: Flippant.Adapters.Redis,
               redis_opts: [url: System.get_env("REDIS_URL"), name: :flippant]
        ```

Usage

Flippant composes three constructs to determine whether a feature is enabled:

  • Actors - An actor can be any value, but typically it is a %User{} or some other struct representing a user.
  • Groups - Groups are used to identify and qualify actors. For example, “everybody”, “nobody”, “admins”, “staff”, “testers” could all be groups names.
  • Rules - Rules represent individual features which are evaluated against actors and groups. For example, “search”, “analytics”, “super-secret-feature” could all be rule names.

Let’s walk through setting up a few example groups and rules. You’ll want to establish groups at startup, as they aren’t likely to change (and defining functions from a web interface isn’t wise).

Groups

First, a group that nobody can belong to. This is useful for disabling a feature without deleting it:

Flippant.register("nobody", fn(_actor, _values) -> false end)

Now the opposite, a group that everybody can belong to:

Flippant.register("everybody", fn(_actor, _values) -> true end)

To be more exclusive and define staff only features we need a “staff” group:

Flippant.register("staff", fn
  nil, _values   -> false
  actor, _values -> actor.staff?
end)

Lastly, we’ll roll out a feature out to a percentage of the actors:

Flippant.register("adopters", fn
  _actor, []         -> false
  %{id: id}, buckets -> rem(id, 10) in buckets
end)

With some core groups defined we can set up some rules now.

Rules

Rules are comprised of a name, a group, and an optional set of values. Starting with a simple example that builds on the groups we have already created, we’ll enable the “search” feature:

# Any staff can use the "search" feature
Flippant.enable("search", "staff")

# 30% of "adopters" can use the "search" feature as well
Flippant.enable("search", "adopters", [0, 1, 2])

Because rules are only built of binaries and simple data they can be defined or refined at runtime. In fact, this is a crucial part of feature toggling. With a web interface rules can be added, removed, or modified.

# Turn search off for adopters
Flippant.disable("search", "adopters")

# On second thought, enable it again for 10%
Flippant.enable("search", "adopters", [3])

With a set of groups and rules defined we can check whether a feature is enabled for a particular actor:

staff_user = %User{id: 1, staff?: true}
early_user = %User{id: 2, staff?: false}
later_user = %User{id: 3, staff?: false}

Flippant.enabled?("search", staff_user) #=> true, staff
Flippant.enabled?("search", early_user) #=> false, not an adopter
Flippant.enabled?("search", later_user) #=> true, is an adopter

If an actor qualifies for multiple groups and any of the rules evaluate to true that feature will be enabled for them. Think of the “nobody” and “everybody” groups that were defined earlier:

Flippant.enable("search", "everybody")
Flippant.enable("search", "nobody")

Flippant.enabled?("search", %User{}) #=> true

Breakdown

Evaluating rules requires a round trip to the database. Clearly, with a lot of rules it is inefficient to evaluate each one individually. There is a function to help with this exact scenario:

Flippant.enable("search", "staff")
Flippant.enable("delete", "everybody")
Flippant.enable("invite", "nobody")

Flippant.breakdown(%User{id: 1, staff?: true}) #=> %{
  "search" => true,
  "delete" => true,
  "invite" => false
}

The breakdown is a simple map of binary keys to boolean values. This is extremely handy for single page applications where you can serialize the breakdown on boot or send it back from an endpoint as JSON.

Configuring Groups

Group definitions are stored in a process, which requires the Flippant application to be started. That means they can’t be defined within a configuration file and should instead be linked from Application.start/2. You can make Flippant.register/2 calls directly from the application module, or put them into a separate module and start it as a temporary worker.

defmodule MyApp do
  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      worker(MyApp.Flippant, [], restart: :temporary)
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]

    Supervisor.start_link(children, opts)
  end
end

Here you can see the worker was defined with restart: :temporary. Now, within the MyApp.Flippant module:

defmodule MyApp.Flippant do
  def start_link do
    Flippant.register("nobody", &nobody?/2)
    Flippant.register("everybody", &everybody?/2)

    :ignore
  end

  def nobody?(_, _), do: false
  def everybody?(_, _), do: true
end

Testing

To avoid touching Redis while testing you can use the Memory adapter. Within config/test.exs override the :adapter:

config :flippant, adapter: Flippant.Adapters.Memory

The memory adapter behaves identically but will clear out whenever the application is restarted.

Customizing Value Serialization

As seen above in [Usage][], it is possible to store a value along with a rule. Values can be any type of data structure, including lists, maps, or even modules. This adds a great deal of additional power to group evaluation, but it means that the values must be serialized in a high fidelity way.

By default, values are stored using Erlang’s binary term storage. This works perfectly fine, but isn’t especially readable and isn’t compatible with other languages. If you’d prefer to use JSON or MessagePack instead you can provide a custom serializer and configure Flippant to use that instead. For example, to use MessagePack via the Msgpax libary:

defmodule MyApp.Serializer do
  @behaviour Flippant.Serializer

  def dump(value), do: Msgpax.pack!(value)
  def load(value), do: Msgpax.unpack!(value)
end

Then, within config.exs set the serializer:

config :flippant, serializer: MyApp.Serializer

License

MIT License, see LICENSE.txt for details.