Dependencies: Libraries or OTP applications

The project: Scrumchkin Online

About a year ago I created a card game to teach Scrum: Scrumchkin. The game made the learning process more fun and was adopted by Scrum Trainers from several countries, until the pandemic made any in-person class unfeasible.

And that’s where my personal project came from: creating an online version of Scrumchkin. Which would be a great opportunity to play and learn more about Phoenix Liveview.

Initially, I thought of the following structure for the project:

This way, it would be possible to create games in separate processes and have a registry with unique identifiers for each game so that each match could be accessed through a different URL.

Example:

  • The user accesses the URL http://scrumchkin.com/game/abc123

  • The web application asks the Game Registry where game abc123 is

  • The Game Registry finds the PID of the match and returns it to the web application

The Game Registry as a library

Keeping in mind the single responsibility principle, the design above makes evident the existence of 3 different projects: The Game Registry, the Game Server and the Web Interface.

The next paragraphs will talk about some technical aspects of Elixir as a curiosity. If you just want to understand the difference between a library and an OTP application feel free to skip this part :)

Technically the Game Registry is extremely simple: it links a unique ID to a match. It’s basically a dictionary that has a UUID as key and a PID of a GenServer as value for a match.

Initially, I created the Game Registry as a library capable of performing CRUD operations on an ets table:

defmodule GameRegister do
  def init() do
    :ets.new(:scrumchkin, [:set, :public, :named_table])
  end

  def save(value) do
    key = UUID.uuid1()
    :ets.insert_new(:scrumchkin, {key, value})
    key
  end

  def delete(key) do
    :ets.delete(:scrumchkin, key)
  end

  def get(key) do
    :scrumchkin
    |> :ets.lookup(key)
    |> format_result
  end

  def list_all do
    :ets.tab2list(:scrumchkin)
  end

  defp format_result([]), do: {:error, "Game not found"}

  defp format_result(item_list) do
    item_list
    |> hd
  end
end

TL;DR - The library stores the current state of matches and links them to an identifier code. It is capable of listing, getting, saving and deleting matches from the registry.

A small problem

For me to use the ets table, it needed to exist. This means that at some point the init function from the code above would need to be called by my web application.

  def init() do
    :ets.new(:scrumchkin, [:set, :public, :named_table])
  end

But this goes against the single responsibility principle I used to divide this project into smaller parts, right?

The Registry as an application

But what is a dependency as a library? It’s a gear that’s part of a whole; something very similar to a Lego piece. We know where the pins and holes are and we use it to build something bigger.

Dependencies as LibrariesThe dependency on an OTP application is a bit different.

Think of a car. Usually, cars have an engine cooling mechanism that starts when you turn the key and start the car. The car depends on this mechanism to work, but it’s somewhat independent: many times it’s activated when we turn off the car (that fan noise that comes from under the hood, especially on hot days).

This cooling mechanism has interfaces with the car’s engine, but controls its own state. There’s a clear relationship of dependency, but not of control. The engine depends on the cooling system not to overheat, but doesn’t control it.

And the same needed to happen with my Game Registry, which ended up like this:

defmodule GameRegister do
  use GenServer

  def start_link(state) do
    GenServer.start_link(__MODULE__, state, name: __MODULE__)
  end

  def init(stack) do
    :ets.new(:scrumchkin, [:set, :public, :named_table])
    IO.puts("Scrumchkin table created")
    {:ok, stack}
  end

  def handle_call({:save, game}, _from, state) do
    key = UUID.uuid1()
    :ets.insert_new(:scrumchkin, {key, game})
    {:reply, key, state}
  end

  def handle_call({:delete, game_id}, _from, state) do
    :ets.delete(:scrumchkin, game_id)
    {:reply, :ok, state}
  end

  def handle_call({:get, game_id}, _from, state) do
    result =
      :scrumchkin
      |> :ets.lookup(game_id)
      |> format_result

    {:reply, result, state}
  end

  def handle_call(:list_all, _from, state) do
    {:reply, :ets.tab2list(:scrumchkin), state}
  end

  def save(game) do
    GenServer.call(__MODULE__, {:save, game})
  end

  def delete(game_id) do
    GenServer.call(__MODULE__, {:delete, game_id})
  end

  def get(game_id) do
    GenServer.call(__MODULE__, {:get, game_id})
  end

  def list_all do
    GenServer.call(__MODULE__, :list_all)
  end

  defp format_result([]), do: {:error, "Game not found"}

  defp format_result(item_list) do
    item_list
    |> hd
  end
end

But… what changes?

My web application is not responsible for creating the ets table. It just says it depends on the Game Registry and that it’s now an extra application.

The change in the mix.exs file is simple:

  def application do
    [
      mod: {Scrumchkin.Application, []},
      extra_applications: [:logger, :runtime_tools, :game_register, :game_engine]
    ]
  end
  defp deps do
    [
      {:game_engine, path: "../game_engine"},
      {:game_register, path: "../game_register"}
    ]
  end

Now, every time I start my application with mix phx.server, my game registry is automatically started and takes on the responsibility of creating the ets table where it will store the PIDs of Scrumchkin matches.

My web application depends on the Game Registry, but trusts that it can solve its problems on its own.

The dangerous failure shaming culture
A reflection on my 20 years working with software development