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
  1. Ensure flippant and redix is started before your application:
def application do
  [applications: [:redix, :flippant]]
end
  1. Set an adapter within your config.exs:
config :flippant, adapter: Flippant.Adapters.Redix

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

You’ll want to put group definitions in config.exs, or in another module that is executed on start.

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.

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.

License

MIT License, see LICENSE.txt for details.