Course
Erlang — The Complete Course
The BEAM, immutability, pattern matching, concurrency via processes, OTP, and supervision — the language behind telecom-grade fault tolerance.
Language course · 6 units
Unit 1: Unit 1 — Values, immutability & pattern matching
Erlang is a functional language: data is immutable, every value is an expression, and functions are pure. Pattern matching IS the assignment — you bind by matching a term against a pattern, which also does the equality and destructuring in one step.
Teach: '=' is a match, not an assignment
X = 5 binds X to 5. 5 = 5 matches (both are 5, success). 6 = 5 fails with a badmatch — this is how you write assertions and destructure. {A, [H | _]} = {ok, [1,2,3]} binds A=ok, H=1. There is no mutation; 'reassignment' is rebinding in a new scope.
Exercises
- Factorial with clauses — fact(0) -> 1; fact(N) -> N * fact(N-1). Write it and note the base case.
- Sum a list with pattern matching — sum([]) -> 0; sum([H|T]) -> H + sum(T).
- Extract a tuple head — Given {ok, Value}, write a function that returns Value in head position of a case.
Practice problems
- Reverse a list (acc)
- Flatten nested lists
- Count occurrences in a list
Unit 2: Unit 2 — Processes, messages & the BEAM scheduler
Concurrency is the language's core: every unit of work is a lightweight process (not an OS thread) managed by the BEAM scheduler, with NO shared state — processes communicate only by copying messages to mailboxes. This gives the 'isolation + fast context switch' foundation for OTP.
Teach: actor model in one example
Pid = spawn(fun() -> receive {ping, From} -> From ! pong end end). Pid ! {ping, self()}. wait: receive pong -> ok end. spawn returns a Pid you can message; each process owns a mailbox. The message copy means no locks, no shared-memory races — a genuine advantage for fault isolation: a crashing process cannot corrupt a sibling's state.
Exercises
- Ping/pong roundtrip — Two processes ping/pong N times; show the message flow and termination.
- Sum work split across processes — Split summing [1..M] across K processes and merge the partial sums with messages.
Practice problems
- A polling worker process
- A simple logging mailbox-process
Unit 3: Unit 3 — OTP: GenServer, supervision, fault tolerance
OTP turns the raw receive loop into reusable behaviours. gen_server gives call/cast/info + a clean state; a supervisor monitors children and restarts them with a configured strategy; a supervision tree is the 'let it crash' architecture. This is what makes Erlang systems self-healing.
Teach: a GenServer counter
gen_server:start_link -> init(Cnt) -> {ok, Cnt}. handle_call(inc, _From, S) -> {reply, S+1, S+1}. Call: gen_server:call(Pid, inc). The state is threaded through handle_*; a crash in the callback does not corrupt the caller — the supervisor restarts the child with its init. State is per-process, isolated and safe.
Exercises
- A GenServer-backed key-value store — Implement get/put on a map held in GenServer state, with a call and a cast.
- One-for-one supervisor restarts a dead child — Describe a supervisor spec where a child crash is restarted and why the tree is self-healing.
Practice problems
- Build a small bounded-buffer queue (GenServer)
- A watchdog process with a timeout
Unit 4: Unit 4 — ETS, distributions & systems thinking
ETS gives in-memory, concurrent, shared (per-process-owned) tables with bag/ordered_set semantics and fast lookups; distributed Erlang spans nodes, but the 'share nothing' message model stays the same. Interviewers value the fallen-over engineering view: design for failure, backpressure, and supervision.
Teach: ETS for a shared cache
Create ETS owned by a process: ets:new(cache, [named_table, public, set, {read_concurrency,true}]). Writes go through the owner; reads can be concurrent. Because ownership is per-process, the table dies with its owner — tying the table lifetime to a supervised process gives you clean restart semantics.
Exercises
- ETS-backed hit counter — A named ETS table counting hits per key, read-heavy, no race on inc.
- Design bounded backpressure into a producer/consumer — Producer generates jobs; consumer is slow. How do you avoid mailbox blow-up?
Practice problems
- ETS LRU cache
- Distributed message relay across two nodes (paper design)
Unit 5: Unit 5 — Immutability & pattern matching
Erlang data is immutable — you never mutate, you rebind and build new values. This is what makes concurrency safe and what makes recursion the default loop. Pattern matching is the heart of the language: it destructures and guards in one step. Master = (match), guards (when), head/tail list recursion, and list comprehensions. Every interview answer should show immutable, recursive, pattern-matched code.
Teach: recursion is the loop
There is no for/while; you recurse. The canonical shape: a base case and a recursive case that consumes the head and recurses on the tail, often carrying an accumulator for tail-call optimization. Example — sum: sum([]) -> 0; sum([H|T]) -> H + sum(T). The BEAM optimizes tail calls, so a tail-recursive accumulator version runs in constant stack. Pattern matching destructures: {ok, Val} = Result binds Val only if the tuple matches.
Exercises
- Reverse a list with an accumulator — Write reverse/1 that reverses a list, tail-recursively.
- Classify numbers with guards — Write classify(N) -> pos | neg | zero using guards.
- Pattern-match a map for a config lookup — Given #{host := H, port := P}, extract and validate the config.
Practice problems
- Tail-recursive reverse/map
- Guard-based classification
- Map destructuring for config
Unit 6: Unit 6 — Supervision trees & fault tolerance
Erlang's fault-tolerance model is 'let it crash': a process that hits an unexpected state dies fast, and its supervisor restarts it from a known-good state. A supervision tree is a hierarchy of supervisors and workers with explicit restart strategies (one_for_one, one_for_all, rest_for_one) and intensity/period limits. This is why Erlang systems survive faults — the recovery is structural, not exception-handling.
Teach: why 'let it crash' is safe
A worker owns a narrow job; if it hits an impossible state, it exits abnormally. The supervisor sees the exit, and per its strategy restarts the worker (one_for_one) or siblings (one_for_all/rest_for_one) within intensity/period. Because state is rebuilt from scratch, there is no corrupt half-state to clean. This trades a rare crash for guaranteed recovery — the opposite of catching everything and limping along.
Exercises
- Choose a restart strategy — A cache worker and a DB writer. If the DB writer crashes, the cache is stale. Which strategy?
- Explain restart intensity/period — A worker crashes in a tight loop. What stops the supervisor from restarting forever?
- link vs monitor — When do you use link/1 vs monitor/1?
Practice problems
- Design a supervision tree for a chat service
- One_for_all cache+writer restart
- Monitor a worker without coupling