Badges & Elixir: Using Behaviours

Svilen Gospodinov
Nov 16, 2017 · 5 min read
Image for post
Image for post
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 module:

# badge.ex
defmodule Badge do

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

end

To make things easier, we define a struct with (name of the badge), (a badge could be granted 1 or more times) and a boolean flag . 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 with a User would then create and pass it as param to our 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 you can specify not just the function name and arity, but also types — here is an Ecto user struct, but you can swap it with any type, including . 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 . 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 is a Map that has a key for every badge and a value with its current “level”. Calling tells us how many times we have awarded that badge (initially ).

The 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 when adopting it!

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

Putting it all together

Back to the 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…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store