Caching in Elixir with Cachex
By: Mike Andrianov
Tags: Phoenix Framework, Elixir

While working on a project which accumulates all user’s Heroku applications and domains for them on a single page, I decided to cache data. I did so because this information is pretty static: changing domains or adding a new application may happen once in a few months or even less. In cases like this, caching is a good option. Response time will be decreased as well as server load. That means that your application will need weaker hardware to run than before applying a caching strategy.

Let me show you how easy and fast you can apply this useful feature for your application by using cachex library.

Elixir has a lot of great features that come out of the box, simply because it runs under the ErlangVM. One of these features is the in-memory database called ETS. So you don’t need anything like Redis, to start store data in memory. Fewer dependencies — easier development, maintenance, and deploys. Cachex uses ETS by default, so we don’t need anything to install besides this library.

Let’s start with adding cachex to mix.exs :

defp deps do

[

...,

{:cachex, "~> 3.2"}

]

end

Then fetch dependency and add Cachex to the children list inside the lib/heroku_lighthouse/application.ex :

defmodule HerokuLighthouse.Application do

use Application def start(_type, _args) do

children = [

HerokuLighthouse.Repo,

HerokuLighthouseWeb.Endpoint,

{Cachex, :cache_warehouse}

] opts = [

strategy: :one_for_one,

name: HerokuLighthouse.Supervisor

]

Supervisor.start_link(children, opts)

end

Currently, I already have a function, that fetches all necessary data from the Heroku API:

defmodule HerokuLighthouse.Dashboard do

defp grouped_apps(user) do

user

|> list_teams

|> fetch_apps_for_teams(user)

|> Map.put(%{name: "Personal"}, personal_apps(user))

|> Enum.reject(fn {_, apps} -> length(apps) == 0 end)

|> Enum.map(fn {team, apps} -> {team, put_domains_to_apps(apps, user)} end)

end ...

end

Because to fetch all necessary data from the Heroku API might be a time-consuming operation, let’s make it asynchronous and put the result to the cache and use it for future requests.

defmodule HerokuLighthouse.Dashboard do

...

defp fetch_apps_async(user) do

Task.start_link(fn ->

fetch_and_cache_apps(user) Phoenix.PubSub.broadcast(

HerokuLighthouse.PubSub,

"dashboard:#{user.id}",

:apps_fetched

)

end) []

end defp fetch_and_cache_apps(user) do

with cached_apps <- grouped_apps(user),

true <-

Cachex.put!(

:cache_warehouse,

"user_#{user.id}_apps",

cached_apps,

ttl: :timer.hours(24)

) do

cached_apps

end

end

end

As you can see fetch_apps_async creates a new async task, which calls fetch_and_cache_apps function and then broadcasts the message after tasks are fetched and returns an empty list immediately. More about broadcasting messages and why I use it here you can read in another my post, called Building real-time search with Phoenix LiveView. fetch_and_cache_apps fetches all data and then caches it for 24 hours.

The next step is to add a function that checks the presence of data in the cache storage and triggers api calls if nothing found inside ETS.

defmodule HerokuLighthouse.Dashboard do

def list_apps(user) do

Cachex.get!(:cache_warehouse, "user_#{user.id}_apps") || fetch_apps_async(user)

end



defp fetch_apps_async(user) do

...

end defp fetch_and_cache_apps(user) do

...

end defp grouped_apps(user) do

...

end

end

The final step is to bust the cache for the current user on logout.

defmodule HerokuLighthouseWeb.Auth do

... def logout(conn) do

cache_key = "user_#{conn.assigns.current_user.id}_apps"

Cachex.del(:cache_warehouse, cache_key) conn

|> configure_session(drop: true)

end

end

That’s all!

Thank you for your time! If you have any questions or ideas, please contact me without any hesitation!