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.
The brief
Build rooms as a Mix project:
Rooms.join(room, user),Rooms.post(room, user, text),Rooms.history(room).- One GenServer per room, started on demand by a DynamicSupervisor and found by name through a Registry.
- Each room keeps the last 50 messages; posting to a room you haven't joined returns
{:error, :not_a_member}. - A crash in one room never affects other rooms.
- Tests with ExUnit for the room logic (pure functions) and the process API.
- 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.
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: 2Run 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.