Elixir Fundamentals: Functional Programming and OTP · Processes, OTP and Fault Tolerance

Capstone: A Chat Room Server

Design a supervised chat room service: one process per room, a registry to find rooms by name, message history with a size limit, and clean handling of crashes. Build it, then review it against the rubric.

Create a free account to save this page, take notes and track your progress. Sign in

The brief

Build rooms as a Mix project:

  1. Rooms.join(room, user), Rooms.post(room, user, text), Rooms.history(room).
  2. One GenServer per room, started on demand by a DynamicSupervisor and found by name through a Registry.
  3. Each room keeps the last 50 messages; posting to a room you haven't joined returns {:error, :not_a_member}.
  4. A crash in one room never affects other rooms.
  5. Tests with ExUnit for the room logic (pure functions) and the process API.
  6. Bonus: put a Phoenix Channel or LiveView in front of it.

A head start

This script shows the core pieces working together: a Registry, a DynamicSupervisor, one GenServer per room, and isolation when one room crashes.

Rooms: Registry + DynamicSupervisor + one GenServer per room
defmodule Room do
  use GenServer
  @limit 3   # small for the demo; 50 in the brief

  def start_link(name), do: GenServer.start_link(__MODULE__, name, name: via(name))
  defp via(name), do: {:via, Registry, {RoomRegistry, name}}

  def ensure(name) do
    case DynamicSupervisor.start_child(RoomSup, {Room, name}) do
      {:ok, _pid} -> :ok
      {:error, {:already_started, _pid}} -> :ok
    end
  end

  def join(room, user), do: (ensure(room); GenServer.call(via(room), {:join, user}))
  def post(room, user, text), do: GenServer.call(via(room), {:post, user, text})
  def history(room), do: GenServer.call(via(room), :history)

  @impl true
  def init(name), do: {:ok, %{name: name, members: MapSet.new(), messages: []}}

  @impl true
  def handle_call({:join, user}, _from, s), do: {:reply, :ok, %{s | members: MapSet.put(s.members, user)}}

  def handle_call({:post, user, text}, _from, s) do
    cond do
      text == "/crash" -> raise "bad message"
      MapSet.member?(s.members, user) ->
        messages = Enum.take([{user, text} | s.messages], @limit)
        {:reply, :ok, %{s | messages: messages}}
      true -> {:reply, {:error, :not_a_member}, s}
    end
  end

  def handle_call(:history, _from, s), do: {:reply, Enum.reverse(s.messages), s}
end

Logger.configure(level: :none)
{:ok, _} = Registry.start_link(keys: :unique, name: RoomRegistry)
{:ok, _} = DynamicSupervisor.start_link(strategy: :one_for_one, name: RoomSup)

Room.join("elixir", "ana")
Room.join("elixir", "ben")
Room.join("rust", "cleo")
for text <- ["hi", "hello!", "who's here?", "me"], do: Room.post("elixir", "ana", text)
IO.inspect(Room.post("elixir", "dev", "let me in"), label: "dev posts")
IO.inspect(Room.history("elixir"), label: "elixir (last 3)")

Room.post("rust", "cleo", "ownership is neat")
try do
  Room.post("elixir", "ben", "/crash")
catch
  :exit, _ -> IO.puts("the elixir room crashed")
end
Process.sleep(50)
IO.inspect(Room.history("rust"), label: "rust room unaffected")
IO.inspect(Room.history("elixir"), label: "elixir room restarted empty")
IO.inspect(DynamicSupervisor.count_children(RoomSup).active, label: "active rooms")

Output

dev posts: {:error, :not_a_member}
elixir (last 3): [{"ana", "hello!"}, {"ana", "who's here?"}, {"ana", "me"}]
the elixir room crashed
rust room unaffected: [{"cleo", "ownership is neat"}]
elixir room restarted empty: []
active rooms: 2

Run this on your computer — see “Get Started” for setup.

Review your work

Open each question for the model answer and rubric.

Deliverables

Process design: what does a strong solution look like?

Model answer

One GenServer per room under a DynamicSupervisor, registered via Registry (no global atom names created from user input); rooms started on demand; the application supervision tree starts the Registry and DynamicSupervisor; a crash in one room is isolated and the room restarts cleanly.

Rubric

Rubric: no single bottleneck process for all rooms; names via Registry {:via, ...}; supervision tree in application.ex. Weak: one GenServer holding every room, or String.to_atom on room names.

State, limits and tests: what does a strong solution look like?

Model answer

Message history capped (Enum.take or a queue) so memory can't grow without bound; membership checks return {:error, :not_a_member}; room logic extracted into pure functions with ExUnit tests; process tests use start_supervised! for isolation; optional persistence (Ecto) for history that must survive restarts.

Rubric

Rubric: bounded memory, clear error tuples, tests that don't leak processes between cases. Bonus: Phoenix PubSub broadcasting new messages to a LiveView.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?