Badges & Elixir: Using Behaviours

Svilen Gospodinov
Nov 16, 2017 · 5 min read
Some of the badges available in Heresy

Who doesn’t love earning badges, even virtual ones? Web apps like StackOverflow, Foursquare and Treehouse are all great examples of how badges can make your app much more engaging, personal and fun.

Badges were one of the things we were keen on implementing at Heresy from the very beginning. We used Elixir’s Behaviours to build an internal Badge API and the first set of badges. Thanks to all the positive feedback — we have kept increasing the list of badges your can earn since then.

If you’re curious about Behaviours or perhaps interested in implementing something similar in your app—keep reading ✌️

Why use Behavours?

In a previously published article — Implementing a flexible notifications system in Elixir using Protocols—there was a brief mention of Behaviours:

Behaviours are similar to Interfaces in other languages — they define a contract, which Elixir modules implementing the behaviour have to follow, thus achieving interoperability. In a nutshell — protocols are about data, behaviours are about modules.

To put this into context: our Badges implementation should allow for a set of conditions to be met, before each badge is granted to the user. Since every badge has its own requirements, we need a standard way of checking if the user is eligible for a specific badge (or number of badges, even!) or not.

That’s where Behaviours come in! Each “badge” could have its own unique logic, organised within a module, but following a common specification. Let’s break it all down.

What’s in a badge

Let’s start with the main Badge module:

# badge.ex
defmodule Badge do

defstruct type: nil, level: 0, eligible?: false

end

To make things easier, we define a %Badge{} struct with type (name of the badge), level (a badge could be granted 1 or more times) and a boolean flag eligible?. The boolean flag would make granting badges easy:

# badge.ex
defmodule Badge do

defstruct type: nil, level: 0, eligible?: false

@doc "Awards user with given badge if eligible."
@spec grant(%Badge{}, struct) :: struct | false
def grant(%Badge{eligible?: false}, _), do: false
def grant(badge, user) do
params = %{"badges" => Map.new([{badge.type, badge.level}])}
Account.update_user(user, params) # save to database
end
end

Passing %Badge{type: "firestarter", level: 1, eligible?: true} with a User would then create %{"badges" => %{"firestarter" => 1}} and pass it as param to our Account.update_user function. This would update the user and persist the Firestarter badge in the database.

Creating and adopting Behaviours

Creating a Behaviour is as easy as specifying the callback functions that all modules adopting it should implement. This way we ensure a common API among all badge implementations:

# badge.ex
defmodule Badge do

defstruct type: nil, level: 0, eligible?: false
@doc "Returns a badge with its eligibility status."
@callback eligible?(%User{}) :: %Badge{}
@doc "Checks the current badge level for the user"
@callback find_level(%User{}) :: integer

# rest of the code same as before ✂️

Using @callback you can specify not just the function name and arity, but also types — here %User{} is an Ecto user struct, but you can swap it with any type, including any. Now let’s create our first badge:

# badge/firestarter.ex
defmodule Badge.Firestarter do
@moduledoc "Awarded when you invite a team member."
@behaviour Badge

def eligible?(user) do
level = find_level(user)
%Badge{type: "firestarter", level: level, eligible?: false}
end
def find_level(user) do
0
end
end

Mystery solved — we give the Firestarter badge to users who have invited someone to join their team! Unfortunately, the code above always returns the Badge as eligible?: false. Let’s fix this:

# badge/firestarter.ex
defmodule Badge.Firestarter do
@moduledoc "Awarded when you invite a team member."
@behaviour Badge

def eligible?(user) do
current = user.badges.firestarter
level = find_level(user)
eligible = if current >= level, do: false, else: true
%Badge{type: "firestarter", level: level, eligible?: eligible}
end
def find_level(user) do
invites_accepted = Account.invites_accepted_from(user)
Enum.count(invites_accepted)
end
end

You can assume that user.badges is a Map that has a key for every badge and a value with its current “level”. Calling user.badges.firestarter tells us how many times we have awarded that badge (initially 0).

The find_level function will then go and check the database to see how many invites have been sent by this user and have been accepted. If five people have accepted your invite, then that’s 5x Firestarter badges!

Finally, we check the current vs expected level and determine whether the user deserves a shiny new badge (or two). Ah, and don’t forget to specify the@behaviour when adopting it!

We complete the logic by returning a %Badge{} struct with the badge name, expected level and eligibility status.

Putting it all together

Back to the badge.ex module: let’s implement a handy helper that would check the eligibility for groups of badges for us:

# badge.ex
defmodule Badge do

# ... code same as before ✂️ ...

@doc "Check if user is eligible for given badges."
@spec check_badges(list, %User{}) :: list(%Badge{})
def check_badges(badges, user) do
Enum.map(badges, fn badge -> badge.eligible?(user) end)
end
end

Which we can now use it like so:

iex> Badge.check_badges([Badge.Firestarter], user)
[%Badge{type: "firestarter", level: 1, eligible?: true}]

Nice—looks like this user is eligible for 1x Firestarter badge! Let’s grant it using the function we implemented before:

[Badge.Firestarter]
|> Badge.check_badges(user)
|> Enum.each(&(Badge.grant(&1, user))

And that’s it! We just implemented our first Badge and we can grant and upgrade it at any time if our users are eligible! 🎉

Adding more badges? Simply create a new badge module, adopt the behaviour and plug it in wherever you’re checking for badges 😎

Conclusion

Behaviours are a great pattern that’s widely applicable to a variety of use cases. Furthermore, it greatly simplifies and isolates your code, making it much more approachable by developers new to the code base. Check out the links at the bottom if you’d like to learn more!

If you found this short article useful and would like to see more content on Elixir — please share and help spread the word by hitting the 👏 button.

Thanks for reading!

Further reading:

I’m Svilen — a full-stack web developer and co-founder at Heresy. We’re always looking for engineers who enjoy working with the latest technologies and solving challenging problems. If you’re curious, check out jobs page!

Heresy Dev

Behind the pixels — lessons, thoughts and ideas on software development.

Thanks to Jack Barker

Svilen Gospodinov

Written by

Co-founder & CTO at @HeresyHQ

Heresy Dev

Behind the pixels — lessons, thoughts and ideas on software development.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade