← Back to all projects

ERLANG FROM FIRST PRINCIPLES PROJECTS

Erlang from First Principles: Project-Based Learning

Your Background: Intermediate Elixir developer who understands concepts (actors, message passing, OTP) but needs to master Erlang syntax, tooling, and the raw BEAM foundation.

Core Concept Analysis

Since you know Elixir, you already understand the what and why of:

  • The actor model and lightweight processes
  • Message passing and mailboxes
  • Supervision trees and fault tolerance
  • GenServer, GenStateMachine, Supervisor behaviors
  • Pattern matching and immutability

What you’re missing in Erlang:

Concept Area What Elixir Abstracts Away
Syntax Erlang uses . to end statements, ; between clauses, , between expressions. Variables are CamelCase, atoms are lowercase.
Records vs Maps Elixir uses %Struct{} which compiles to maps. Erlang traditionally used records (compile-time tuples).
OTP Behaviors Elixir’s use GenServer hides the callback structure. Raw Erlang behaviors expose everything.
String Handling Elixir has "binary strings". Erlang has "charlists" (lists of integers) and <<"binaries">>.
Tooling mix is Elixir-specific. Erlang uses rebar3, erlang.mk, or raw erlc.
Macros Elixir macros are elegant. Erlang uses parse transforms (more powerful, more dangerous).
Module System Elixir namespaces with defmodule Foo.Bar. Erlang modules are flat atoms.
BEAM Internals Elixir hides bytecode. Erlang exposes it via beam_lib, -S flags, etc.

The Learning Path

┌─────────────────────────────────────────────────────────────────┐
│ PHASE 1: SYNTAX & BASICS (Projects 1-3)                         │
│ Goal: Write Erlang fluently, understand the differences         │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ PHASE 2: CONCURRENCY RAW (Projects 4-6)                         │
│ Goal: Processes, links, monitors WITHOUT OTP abstractions       │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ PHASE 3: OTP MASTERY (Projects 7-10)                            │
│ Goal: Implement behaviors from scratch, understand internals    │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ PHASE 4: BEAM INTERNALS (Projects 11-14)                        │
│ Goal: Understand what the VM actually does                      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ PHASE 5: ADVANCED & INTEGRATION (Projects 15-18)                │
│ Goal: Distributed systems, NIFs, releases, metaprogramming      │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│ CAPSTONE: PRODUCTION SYSTEM                                     │
│ Goal: Build a complete, production-ready Erlang system          │
└─────────────────────────────────────────────────────────────────┘

PHASE 1: SYNTAX & BASICS


Project 1: Erlang Calculator REPL

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: Prolog, Haskell, Standard ML
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 1. The “Resume Gold” (Educational/Personal Brand)
  • Difficulty: Level 1: Beginner (The Tinkerer)
  • Knowledge Area: Language Syntax / REPL Design
  • Software or Tool: Erlang Shell (erl)
  • Main Book: “Learn You Some Erlang for Great Good!” by Fred Hébert

What you’ll build: An interactive calculator that reads expressions like 3 + 4 * 2, parses them respecting operator precedence, and prints results—all in pure Erlang with a custom REPL loop.

Why it teaches Erlang: This forces you to internalize Erlang’s unique syntax: periods ending expressions, semicolons separating clauses, commas between sequential expressions. You’ll use guards, pattern matching on lists, and recursion—all in Erlang’s style.

Core challenges you’ll face:

  • Tokenizing input (splitting "3+4*2" into [{num,3},{op,add},{num,4},{op,mul},{num,2}]) → maps to Erlang string/list handling
  • Recursive descent parsing with precedence → maps to pattern matching on lists
  • REPL loop (read -> eval -> print -> loop) → maps to tail recursion
  • Error handling (invalid input shouldn’t crash) → maps to Erlang try/catch

Key Concepts:

  • Erlang Syntax Basics: “Learn You Some Erlang” Chapter 2 - Fred Hébert
  • Pattern Matching: “Programming Erlang” Chapter 4 - Joe Armstrong
  • Recursion: “Learn You Some Erlang” Chapter 5 (Recursion) - Fred Hébert
  • Guards: “Programming Erlang” Chapter 4.4 - Joe Armstrong

Difficulty: Beginner Time estimate: Weekend Prerequisites: Basic programming knowledge, have Erlang installed

Real world outcome:

$ erl
1> c(calc).
{ok,calc}
2> calc:start().
calc> 3 + 4 * 2
11
calc> (3 + 4) * 2
14
calc> 10 / 3
3.3333333333333335
calc> quit
bye
ok

Implementation Hints: Your REPL module will look something like:

-module(calc).
-export([start/0]).

start() -> loop().

loop() ->
    case io:get_line("calc> ") of
        "quit\n" -> io:format("bye~n");
        Line ->
            Result = eval(parse(tokenize(Line))),
            io:format("~p~n", [Result]),
            loop()
    end.

Note the Erlang conventions:

  • Module name matches filename (calc.erl)
  • Export list explicitly declares public functions
  • io:get_line/1 returns string with newline
  • io:format/2 uses ~p for pretty-print, ~n for newline
  • The . ends the function, ; would separate clauses, , separates expressions

Learning milestones:

  1. REPL loops and prints → You understand basic Erlang I/O
  2. Tokenizer handles all operators → You understand list processing in Erlang
  3. Precedence works correctly → You’ve mastered recursive pattern matching

Project 2: INI File Parser & Writer

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: OCaml, Haskell, Rust
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 2. The “Micro-SaaS / Pro Tool” (Solo-Preneur Potential)
  • Difficulty: Level 1: Beginner (The Tinkerer)
  • Knowledge Area: File I/O / Parsing
  • Software or Tool: INI Configuration Files
  • Main Book: “Programming Erlang” by Joe Armstrong

What you’ll build: A library that reads INI configuration files into Erlang data structures and writes them back, handling sections, key-value pairs, and comments.

Why it teaches Erlang: INI parsing exercises Erlang’s file I/O, binary/string handling (the <<"binary">> vs "list" distinction), and record or map structures. You’ll also learn rebar3 for project structure.

Core challenges you’ll face:

  • File reading (file:read_file/1 returns binary) → maps to binary vs list strings
  • Line-by-line parsing → maps to binary pattern matching
  • Data representation (sections → keys → values) → maps to maps or records
  • Writing back to file → maps to iolist construction

Key Concepts:

  • Binaries and Strings: “Learn You Some Erlang” Chapter 6 - Fred Hébert
  • File I/O: “Programming Erlang” Chapter 13 - Joe Armstrong
  • Maps: “Programming Erlang” Chapter 5.3 - Joe Armstrong
  • Records: “Learn You Some Erlang” Chapter 7 - Fred Hébert

Difficulty: Beginner Time estimate: Weekend Prerequisites: Completed Project 1

Real world outcome:

$ cat config.ini
[database]
host = localhost
port = 5432
; This is a comment
[cache]
enabled = true

$ erl
1> c(ini).
{ok,ini}
2> {ok, Config} = ini:read("config.ini").
{ok,#{database => #{host => <<"localhost">>, port => <<"5432">>},
      cache => #{enabled => <<"true">>}}}
3> NewConfig = maps:put(redis, #{host => <<"127.0.0.1">>}, Config).
4> ini:write("config_new.ini", NewConfig).
ok

Implementation Hints: Binary pattern matching in Erlang is powerful:

parse_line(<<"[", Rest/binary>>) ->
    %% Section header: extract name before "]"
    ...;
parse_line(<<";", _/binary>>) ->
    %% Comment line, skip
    comment;
parse_line(Line) ->
    %% Key = Value line
    case binary:split(Line, <<"=">>) of
        [Key, Value] -> {kv, trim(Key), trim(Value)};
        _ -> error
    end.

Erlang binaries use <<...>> syntax, and /binary in patterns means “rest of the binary.”

Learning milestones:

  1. Read file into binary → You understand Erlang file operations
  2. Parse all line types correctly → You’ve mastered binary pattern matching
  3. Round-trip (read then write) preserves data → You understand iolists

Project 3: Markdown to HTML Converter

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: Haskell, OCaml, Rust
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool” (Solo-Preneur Potential)
  • Difficulty: Level 2: Intermediate (The Developer)
  • Knowledge Area: Parsing / Text Processing
  • Software or Tool: Markdown
  • Main Book: “Programming Erlang” by Joe Armstrong

What you’ll build: A Markdown parser that converts a subset of Markdown (headers, bold, italic, links, code blocks, lists) to HTML.

Why it teaches Erlang: This is a significant parsing project that forces you to think in Erlang idioms. You’ll handle nested structures (lists within lists), inline formatting within block elements, and produce well-formed HTML output using iolists.

Core challenges you’ll face:

  • Block vs inline parsing (paragraphs contain bold/italic) → maps to two-pass parsing
  • Nested lists → maps to recursive data structures
  • State tracking (inside code block? inside list?) → maps to accumulator patterns
  • HTML escaping (prevent XSS) → maps to binary transformation

Key Concepts:

  • Recursive Data Structures: “Learn You Some Erlang” Chapter 5 - Fred Hébert
  • Accumulators: “Programming Erlang” Chapter 4.2 - Joe Armstrong
  • IOLists: “Learn You Some Erlang” Chapter 6.3 - Fred Hébert
  • Parsing Techniques: “Language Implementation Patterns” Chapter 2 - Terence Parr

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Completed Projects 1-2

Real world outcome:

$ cat README.md
# Hello World

This is **bold** and *italic*.

- Item 1
- Item 2
  - Nested item

$ erl
1> c(markdown).
{ok,markdown}
2> {ok, Md} = file:read_file("README.md").
3> Html = markdown:to_html(Md).
<<"<h1>Hello World</h1>\n<p>This is <strong>bold</strong> and <em>italic</em>.</p>\n<ul><li>Item 1</li><li>Item 2<ul><li>Nested item</li></ul></li></ul>">>
4> file:write_file("README.html", Html).
ok

Open README.html in a browser to verify correct rendering.

Implementation Hints: Use a two-pass approach:

  1. Block pass: Split into paragraphs, headers, lists, code blocks
  2. Inline pass: Within each block, parse bold, italic, links, code spans

For nested lists, track indentation level and use recursion:

parse_list(Lines, Indent) ->
    case next_line_indent(Lines) of
        MoreIndent when MoreIndent > Indent ->
            {Nested, Rest} = parse_list(Lines, MoreIndent),
            ...;
        _ ->
            ...
    end.

Learning milestones:

  1. Headers and paragraphs work → Basic block parsing done
  2. Inline formatting works → You understand recursive descent in Erlang
  3. Nested lists render correctly → You’ve mastered recursive data structures

PHASE 2: CONCURRENCY RAW


Project 4: Chat Server Without OTP

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: Go, Rust, Haskell
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool” (Solo-Preneur Potential)
  • Difficulty: Level 2: Intermediate (The Developer)
  • Knowledge Area: Concurrency / Networking
  • Software or Tool: TCP Sockets
  • Main Book: “Programming Erlang” by Joe Armstrong

What you’ll build: A multi-user TCP chat server where each client gets their own process, messages broadcast to all users, and crashes don’t bring down the server—all using raw spawn, spawn_link, and message passing without any OTP behaviors.

Why it teaches Erlang: This is why Erlang exists. Building this without OTP shows you exactly what problems OTP solves. You’ll implement your own “supervisor” logic, handle process death, and manage shared state across processes.

Core challenges you’ll face:

  • Accepting TCP connections → maps to gen_tcp module
  • One process per client → maps to spawn and message passing
  • Broadcasting messages → maps to maintaining process registry
  • Handling client disconnection → maps to monitors and exit signals
  • Server state (list of connected clients) → maps to stateful receive loops

Key Concepts:

  • Processes: “Programming Erlang” Chapter 8 - Joe Armstrong
  • Message Passing: “Learn You Some Erlang” Chapter 10 - Fred Hébert
  • Links and Monitors: “Learn You Some Erlang” Chapter 12 - Fred Hébert
  • TCP Sockets: “Programming Erlang” Chapter 14 - Joe Armstrong

Difficulty: Intermediate Time estimate: 1-2 weeks Prerequisites: Completed Phase 1

Real world outcome:

# Terminal 1 - Start server
$ erl
1> c(chat_server).
{ok,chat_server}
2> chat_server:start(4000).
Listening on port 4000...

# Terminal 2 - Client 1
$ telnet localhost 4000
Enter name: Alice
Alice joined the chat
Bob: Hello everyone!

# Terminal 3 - Client 2
$ telnet localhost 4000
Enter name: Bob
Bob joined the chat
> Hello everyone!
Alice: Hi Bob!

# Kill Terminal 2 (Ctrl+C)
# Server continues, Terminal 3 sees: "Alice has left"

Implementation Hints: The architecture:

                    ┌─────────────┐
                    │ Acceptor    │ (accepts connections)
                    └──────┬──────┘
                           │ spawns
            ┌──────────────┼──────────────┐
            ▼              ▼              ▼
    ┌───────────┐  ┌───────────┐  ┌───────────┐
    │ Client 1  │  │ Client 2  │  │ Client 3  │
    └─────┬─────┘  └─────┬─────┘  └─────┬─────┘
          │              │              │
          └──────────────┼──────────────┘
                         ▼
                  ┌─────────────┐
                  │ Room Process│ (maintains client list)
                  └─────────────┘

Key Erlang pattern - the receive loop with state:

room_loop(Clients) ->
    receive
        {join, Pid, Name} ->
            monitor(process, Pid),
            broadcast(Clients, {joined, Name}),
            room_loop([{Pid, Name} | Clients]);
        {message, FromPid, Text} ->
            Name = find_name(FromPid, Clients),
            broadcast(Clients, {msg, Name, Text}),
            room_loop(Clients);
        {'DOWN', _, process, Pid, _} ->
            Name = find_name(Pid, Clients),
            NewClients = lists:keydelete(Pid, 1, Clients),
            broadcast(NewClients, {left, Name}),
            room_loop(NewClients)
    end.

Learning milestones:

  1. Single client can connect and send messages → Basic gen_tcp working
  2. Multiple clients see each other’s messages → Broadcasting works
  3. Client disconnect doesn’t crash server → You understand monitors

Project 5: Process Pool Manager

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: Go, Rust, C
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model (B2B Utility)
  • Difficulty: Level 3: Advanced (The Engineer)
  • Knowledge Area: Concurrency / Resource Management
  • Software or Tool: Worker Pool
  • Main Book: “Erlang and OTP in Action” by Logan, Merritt, Carlsson

What you’ll build: A worker pool that maintains N worker processes, distributes tasks to available workers, queues tasks when all workers are busy, and restarts crashed workers—without using poolboy or OTP behaviors.

Why it teaches Erlang: Pool management is exactly what OTP supervisors and gen_server abstract away. Building it raw teaches you why those abstractions exist. You’ll handle the tricky edge cases: what if a worker dies mid-task? What if the pool manager dies?

Core challenges you’ll face:

  • Worker lifecycle (spawn, monitor, restart) → maps to process management
  • Task distribution (round-robin vs least-loaded) → maps to state management
  • Queue management (tasks waiting for workers) → maps to queue data structure
  • Handling worker crashes → maps to monitors and restart logic
  • Graceful shutdown (wait for tasks to complete) → maps to coordinated termination

Key Concepts:

  • Monitors vs Links: “Learn You Some Erlang” Chapter 12 - Fred Hébert
  • Process Registry: “Programming Erlang” Chapter 8.4 - Joe Armstrong
  • Queue Module: Erlang Documentation - queue module
  • Restart Strategies: “Erlang and OTP in Action” Chapter 4 - Logan et al.

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Completed Project 4

Real world outcome:

$ erl
1> c(pool).
{ok,pool}
2> Pool = pool:start(3).  % 3 workers
<0.85.0>
3> pool:status(Pool).
{workers, 3, idle, 3, busy, 0, queue, 0}

4> % Submit 5 tasks (3 run immediately, 2 queue)
4> [pool:submit(Pool, fun() -> timer:sleep(1000), io:format("done~n") end) || _ <- lists:seq(1,5)].
5> pool:status(Pool).
{workers, 3, idle, 0, busy, 3, queue, 2}

% After 1 second:
done
done
done
6> pool:status(Pool).
{workers, 3, idle, 1, busy, 2, queue, 0}

% After another second:
done
done
7> pool:status(Pool).
{workers, 3, idle, 3, busy, 0, queue, 0}

8> % Test crash handling - worker should restart
8> pool:submit(Pool, fun() -> exit(crash) end).
9> pool:status(Pool).  % Still 3 workers!
{workers, 3, idle, 3, busy, 0, queue, 0}

Implementation Hints: Pool manager state structure:

-record(pool_state, {
    size        :: integer(),
    workers     :: [pid()],        % All worker PIDs
    available   :: [pid()],        % Idle workers
    busy        :: [{pid(), ref()}], % {Worker, TaskRef}
    queue       :: queue:queue()   % Waiting tasks
}).

Key insight: Use monitors, not links. Links propagate crashes bidirectionally. Monitors just notify you when a process dies:

start_worker() ->
    Pid = spawn(fun worker_loop/0),
    monitor(process, Pid),
    Pid.

When you receive {'DOWN', Ref, process, Pid, Reason}, spawn a replacement and redistribute any queued tasks.

Learning milestones:

  1. Fixed pool runs tasks → Basic pool works
  2. Tasks queue when busy → State management correct
  3. Workers restart after crash → Monitor handling works
  4. Pool handles backpressure (rejects when queue too long) → Production-ready thinking

Project 6: Rate Limiter with Token Bucket

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: Go, Rust, C
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model (B2B Utility)
  • Difficulty: Level 2: Intermediate (The Developer)
  • Knowledge Area: Algorithms / Concurrency
  • Software or Tool: API Rate Limiting
  • Main Book: “Programming Erlang” by Joe Armstrong

What you’ll build: A token bucket rate limiter that limits requests to N per second per client, using ETS for shared state and a refill process that adds tokens periodically.

Why it teaches Erlang: This introduces ETS (Erlang Term Storage), the in-memory key-value store that enables shared mutable state in the otherwise immutable Erlang world. You’ll also handle timing with erlang:send_after/3.

Core challenges you’ll face:

  • ETS table management (create, read, update atomically) → maps to ETS operations
  • Atomic operations (check-and-decrement must be atomic) → maps to ets:update_counter/3
  • Token refill timing → maps to erlang:send_after/3
  • Multiple buckets (per-client rate limiting) → maps to table design
  • Cleanup (remove stale client entries) → maps to garbage collection patterns

Key Concepts:

  • ETS Tables: “Learn You Some Erlang” Chapter 14 - Fred Hébert
  • ETS Advanced Operations: “Erlang and OTP in Action” Chapter 6 - Logan et al.
  • Timers: “Programming Erlang” Chapter 8.7 - Joe Armstrong
  • Atomic Updates: Erlang Documentation - ets:update_counter/3

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Completed Project 4

Real world outcome:

$ erl
1> c(rate_limiter).
{ok,rate_limiter}
2> rate_limiter:start(10, 1000).  % 10 requests per 1000ms
ok

3> % Make 10 requests quickly (all should succeed)
3> [rate_limiter:check(client1) || _ <- lists:seq(1,10)].
[ok,ok,ok,ok,ok,ok,ok,ok,ok,ok]

4> % 11th request should be rate limited
4> rate_limiter:check(client1).
{error, rate_limited}

5> % Different client has its own bucket
5> rate_limiter:check(client2).
ok

6> % Wait 1 second, client1 has tokens again
6> timer:sleep(1000).
7> rate_limiter:check(client1).
ok

Implementation Hints: Create ETS table with atomic counter support:

init(MaxTokens, RefillMs) ->
    ets:new(?TABLE, [named_table, public, set]),
    spawn(fun() -> refill_loop(MaxTokens, RefillMs) end).

The check function uses atomic update:

check(ClientId) ->
    case ets:update_counter(?TABLE, ClientId, {2, -1, 0, 0}, {ClientId, MaxTokens}) of
        N when N > 0 -> ok;
        0 -> {error, rate_limited}
    end.

The cryptic {2, -1, 0, 0} means: “Update field 2, subtract 1, but floor at 0, and if already 0 return 0.”

Learning milestones:

  1. Basic rate limiting works → ETS fundamentals understood
  2. Tokens refill correctly → Timer handling works
  3. High concurrency (1000 clients) works → ETS scales

PHASE 3: OTP MASTERY


Project 7: Build Your Own GenServer

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (Erlang-specific)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold” (Educational/Personal Brand)
  • Difficulty: Level 3: Advanced (The Engineer)
  • Knowledge Area: OTP Internals / Behavior Design
  • Software or Tool: OTP Behaviors
  • Main Book: “Erlang and OTP in Action” by Logan, Merritt, Carlsson

What you’ll build: A simplified gen_server behavior from scratch that handles call (synchronous), cast (asynchronous), info (other messages), and proper termination—understanding exactly how the real gen_server works.

Why it teaches Erlang: This demystifies OTP completely. You’ll understand why handle_call returns {reply, Reply, NewState}, why call blocks until response, and how gen_server manages the receive loop.

Core challenges you’ll face:

  • Synchronous call/reply (gen_server:call blocks) → maps to reference-based reply matching
  • Handling all message types (call, cast, info) → maps to message tagging
  • Callback invocation (calling module’s handle_* functions) → maps to module as parameter
  • Timeout handling → maps to receive timeouts
  • Clean termination → maps to terminate callback

Key Concepts:

  • GenServer Internals: “Erlang and OTP in Action” Chapter 3 - Logan et al.
  • Behaviors: “Learn You Some Erlang” Chapter 13 - Fred Hébert
  • Receive with Timeout: “Programming Erlang” Chapter 8.6 - Joe Armstrong
  • References for Reply Matching: “Programming Erlang” Chapter 8.8 - Joe Armstrong

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Completed Phase 2

Real world outcome:

$ erl
1> c(my_gen_server), c(counter_example).
{ok,counter_example}

%% counter_example uses my_gen_server behavior:
2> {ok, Pid} = counter_example:start_link(0).
{ok,<0.85.0>}

3> counter_example:increment(Pid).
ok  % This was a cast (async)

4> counter_example:get(Pid).
1   % This was a call (sync)

5> counter_example:increment(Pid).
6> counter_example:increment(Pid).
7> counter_example:get(Pid).
3

8> counter_example:stop(Pid).
ok  % Clean termination

Implementation Hints: The core receive loop:

loop(Module, State) ->
    receive
        {'$call', From, Request} ->
            case Module:handle_call(Request, From, State) of
                {reply, Reply, NewState} ->
                    reply(From, Reply),
                    loop(Module, NewState);
                {noreply, NewState} ->
                    loop(Module, NewState);
                {stop, Reason, Reply, NewState} ->
                    reply(From, Reply),
                    Module:terminate(Reason, NewState)
            end;
        {'$cast', Request} ->
            case Module:handle_cast(Request, State) of
                {noreply, NewState} ->
                    loop(Module, NewState);
                {stop, Reason, NewState} ->
                    Module:terminate(Reason, NewState)
            end;
        Info ->
            case Module:handle_info(Info, State) of
                {noreply, NewState} ->
                    loop(Module, NewState);
                {stop, Reason, NewState} ->
                    Module:terminate(Reason, NewState)
            end
    end.

The call function that blocks:

call(Pid, Request) ->
    Ref = make_ref(),
    Pid ! {'$call', {self(), Ref}, Request},
    receive
        {Ref, Reply} -> Reply
    after 5000 ->
        exit(timeout)
    end.

reply({Pid, Ref}, Reply) ->
    Pid ! {Ref, Reply}.

Learning milestones:

  1. Call/cast work → You understand the basic pattern
  2. Timeout works → You understand receive timeouts
  3. Use your behavior to build a real module → It’s actually usable!

Project 8: Supervision Tree Visualizer

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: Elixir (for comparison), Go
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 2. The “Micro-SaaS / Pro Tool” (Solo-Preneur Potential)
  • Difficulty: Level 3: Advanced (The Engineer)
  • Knowledge Area: OTP / Introspection
  • Software or Tool: OTP Supervisor
  • Main Book: “Designing for Scalability with Erlang/OTP” by Cesarini & Vinoski

What you’ll build: A tool that introspects a running Erlang application’s supervision tree and outputs an ASCII or Graphviz visualization showing supervisors, workers, their strategies, and current status.

Why it teaches Erlang: You’ll learn the introspection APIs (supervisor:which_children/1, sys:get_status/1, etc.) and deeply understand how supervision trees are structured. This is knowledge that transfers directly to debugging production systems.

Core challenges you’ll face:

  • Introspection (getting supervisor children) → maps to supervisor module API
  • Recursive tree walking → maps to nested supervisor handling
  • Process information (memory, message queue, state) → maps to sys module
  • Output formatting (ASCII tree or DOT format) → maps to string building
  • Registered names vs PIDs → maps to process registration

Key Concepts:

  • Supervisor API: “Erlang and OTP in Action” Chapter 4 - Logan et al.
  • sys Module: “Learn You Some Erlang” Chapter 14.3 - Fred Hébert
  • Process Info: “Programming Erlang” Chapter 8.9 - Joe Armstrong
  • Introspection: “Designing for Scalability” Chapter 5 - Cesarini & Vinoski

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Understanding of OTP supervisors

Real world outcome:

$ erl -pa ebin
1> application:start(my_app).
ok

2> sup_viz:print(my_app_sup).
my_app_sup (supervisor, one_for_one)
├── db_pool_sup (supervisor, one_for_all)
   ├── db_conn_1 (gen_server) [memory: 45KB, msgs: 0]
   ├── db_conn_2 (gen_server) [memory: 42KB, msgs: 0]
   └── db_conn_3 (gen_server) [memory: 43KB, msgs: 2]
├── cache_server (gen_server) [memory: 128KB, msgs: 0]
└── web_acceptor_sup (supervisor, simple_one_for_one)
    ├── conn_handler_1 (gen_server) [memory: 12KB, msgs: 0]
    ├── conn_handler_2 (gen_server) [memory: 11KB, msgs: 1]
    └── ... (47 more children)

3> sup_viz:to_dot(my_app_sup, "tree.dot").
ok
$ dot -Tpng tree.dot -o tree.png

Implementation Hints: Getting supervisor children:

get_children(SupPid) ->
    supervisor:which_children(SupPid).
    % Returns: [{Id, Pid, Type, Modules}, ...]
    % Type is 'worker' or 'supervisor'

Getting process info:

get_info(Pid) ->
    case erlang:process_info(Pid, [memory, message_queue_len, registered_name]) of
        undefined -> dead;
        Info -> Info
    end.

For nested supervisors, recurse:

walk_tree(SupPid, Depth) ->
    Children = get_children(SupPid),
    lists:map(fun({Id, Pid, Type, _Mods}) ->
        case Type of
            supervisor ->
                {Id, Pid, supervisor, walk_tree(Pid, Depth+1)};
            worker ->
                {Id, Pid, worker, get_info(Pid)}
        end
    end, Children).

Learning milestones:

  1. Single-level supervisor visualized → Basic introspection works
  2. Nested trees render correctly → Recursive walking works
  3. Include process health metrics → Deep introspection mastered

Project 9: Distributed Key-Value Store with Mnesia

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (Mnesia is Erlang-specific)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 4. The “Open Core” Infrastructure (Enterprise Scale)
  • Difficulty: Level 4: Expert (The Systems Architect)
  • Knowledge Area: Distributed Systems / Databases
  • Software or Tool: Mnesia
  • Main Book: “Erlang and OTP in Action” by Logan, Merritt, Carlsson

What you’ll build: A distributed key-value store using Mnesia that replicates data across multiple Erlang nodes, handles node failures gracefully, and provides both RAM and disk-backed storage.

Why it teaches Erlang: Mnesia is Erlang’s built-in distributed database—something no other language has. Learning Mnesia teaches you Erlang’s unique approach to data persistence, transactions, and distributed systems.

Core challenges you’ll face:

  • Schema creation (tables, attributes, indexes) → maps to mnesia:create_table/2
  • Node clustering → maps to distributed Erlang nodes
  • Replication (RAM vs disc copies) → maps to table types
  • Transactions (reading, writing, rollback) → maps to mnesia:transaction/1
  • Network partitions → maps to mnesia:set_master_nodes/2

Key Concepts:

  • Mnesia Basics: “Erlang and OTP in Action” Chapter 6 - Logan et al.
  • Distributed Erlang: “Programming Erlang” Chapter 10 - Joe Armstrong
  • Transactions: “Learn You Some Erlang” Chapter 15 - Fred Hébert
  • CAP Theorem: “Designing Data-Intensive Applications” Chapter 5 - Martin Kleppmann

Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Completed Projects 7-8

Real world outcome:

# Terminal 1 - Start node1
$ erl -sname node1
(node1@localhost)1> kvstore:start().
ok
(node1@localhost)2> kvstore:put(mykey, "hello").
ok

# Terminal 2 - Start node2 and join cluster
$ erl -sname node2
(node2@localhost)1> kvstore:join('node1@localhost').
ok
(node2@localhost)2> kvstore:get(mykey).
{ok, "hello"}  % Replicated!

# Kill Terminal 1 (Ctrl+C twice)
# Terminal 2 still works:
(node2@localhost)3> kvstore:get(mykey).
{ok, "hello"}  % Data persisted!

# Restart Terminal 1:
$ erl -sname node1
(node1@localhost)1> kvstore:rejoin().
ok
(node1@localhost)2> kvstore:get(mykey).
{ok, "hello"}  % Data recovered!

Implementation Hints: Schema creation:

create_schema(Nodes) ->
    mnesia:create_schema(Nodes),
    mnesia:start(),
    mnesia:create_table(kv, [
        {attributes, [key, value, timestamp]},
        {disc_copies, Nodes},  % Persist to disk
        {type, set}
    ]).

Transactions:

put(Key, Value) ->
    mnesia:transaction(fun() ->
        mnesia:write(#kv{key=Key, value=Value, timestamp=erlang:system_time()})
    end).

get(Key) ->
    mnesia:transaction(fun() ->
        case mnesia:read(kv, Key) of
            [#kv{value=V}] -> {ok, V};
            [] -> not_found
        end
    end).

Joining a cluster:

join(ExistingNode) ->
    pong = net_adm:ping(ExistingNode),
    mnesia:change_config(extra_db_nodes, [ExistingNode]),
    mnesia:add_table_copy(kv, node(), disc_copies).

Learning milestones:

  1. Single-node storage works → Mnesia basics understood
  2. Two-node replication works → Distributed Mnesia works
  3. Node failure and recovery handled → Production patterns learned

Project 10: State Machine with gen_statem

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: Go, Rust, Haskell
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model (B2B Utility)
  • Difficulty: Level 3: Advanced (The Engineer)
  • Knowledge Area: State Machines / Protocol Implementation
  • Software or Tool: gen_statem
  • Main Book: “Designing for Scalability with Erlang/OTP” by Cesarini & Vinoski

What you’ll build: A TCP connection state machine implementing a protocol (like a simplified FTP or SMTP) using gen_statem, handling states like greeting, authenticating, authenticated, transferring, etc.

Why it teaches Erlang: gen_statem is OTP’s formal state machine behavior. It’s more powerful than gen_server for protocol implementations. You’ll learn callback mode (state functions vs handle_event), state enter actions, and timeout handling.

Core challenges you’ll face:

  • State definition (which states, what transitions) → maps to state function callbacks
  • Event handling (what events trigger transitions) → maps to event types
  • State enter actions (actions on entering a state) → maps to enter callbacks
  • Timeouts (idle timeout, operation timeout) → maps to gen_statem timeouts
  • Integration with gen_tcp → maps to active vs passive sockets

Key Concepts:

  • gen_statem: “Designing for Scalability” Chapter 7 - Cesarini & Vinoski
  • State Machines: “Learn You Some Erlang” Chapter 13.5 - Fred Hébert
  • TCP with OTP: “Programming Erlang” Chapter 14 - Joe Armstrong
  • Protocol Design: RFC specifications for chosen protocol

Difficulty: Advanced Time estimate: 2 weeks Prerequisites: Completed Projects 7-8

Real world outcome:

# Terminal 1 - Start FTP server
$ erl
1> c(ftp_server).
{ok,ftp_server}
2> ftp_server:start(2121).
Listening on port 2121...

# Terminal 2 - Connect with telnet
$ telnet localhost 2121
220 Welcome to Erlang FTP
USER anonymous
331 Password required
PASS guest
230 Login successful
PWD
257 "/" is current directory
LIST
150 Opening data connection
-rw-r--r-- 1 user user 1024 Jan 01 12:00 file.txt
226 Transfer complete
QUIT
221 Goodbye

Implementation Hints: gen_statem with state functions (each state is a callback):

-module(ftp_statem).
-behaviour(gen_statem).
-export([callback_mode/0, init/1]).
-export([greeting/3, waiting_password/3, authenticated/3]).

callback_mode() -> [state_functions, state_enter].

init([Socket]) ->
    {ok, greeting, #{socket => Socket}}.

greeting(enter, _OldState, Data) ->
    send(Data, "220 Welcome to Erlang FTP\r\n"),
    {keep_state, Data};
greeting({call, From}, {command, "USER", _User}, Data) ->
    {next_state, waiting_password, Data, [{reply, From, ok}]};
greeting({call, From}, {command, _, _}, Data) ->
    send(Data, "530 Please login first\r\n"),
    {keep_state, Data, [{reply, From, ok}]}.

waiting_password(enter, _OldState, Data) ->
    send(Data, "331 Password required\r\n"),
    {keep_state, Data};
waiting_password({call, From}, {command, "PASS", _Pass}, Data) ->
    {next_state, authenticated, Data, [{reply, From, ok}]}.

authenticated(enter, _OldState, Data) ->
    send(Data, "230 Login successful\r\n"),
    {keep_state, Data}.

Learning milestones:

  1. State transitions work → Basic gen_statem understood
  2. Timeouts work (kick idle clients) → Advanced gen_statem features
  3. Full protocol implemented → Ready for production protocols

PHASE 4: BEAM INTERNALS


Project 11: BEAM Bytecode Analyzer

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (BEAM-specific)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 1. The “Resume Gold” (Educational/Personal Brand)
  • Difficulty: Level 4: Expert (The Systems Architect)
  • Knowledge Area: Compiler Internals / VM Internals
  • Software or Tool: BEAM VM
  • Main Book: “The BEAM Book” by Erik Stenman

What you’ll build: A tool that reads compiled .beam files, disassembles them, and provides analysis: function size, pattern match complexity, tail-call optimization presence, and call graphs.

Why it teaches Erlang: This opens the hood of the BEAM. You’ll understand what your Erlang code compiles to, why some patterns are faster than others, and how the BEAM executes code.

Core challenges you’ll face:

  • BEAM file format (chunks: Code, Atom, StrT, etc.) → maps to beam_lib module
  • Instruction decoding → maps to BEAM instruction set
  • Call graph construction → maps to graph algorithms
  • Identifying optimization opportunities → maps to understanding BEAM optimizations

Key Concepts:

  • BEAM Architecture: “The BEAM Book” Chapters 1-3 - Erik Stenman
  • beam_lib Module: Erlang Documentation
  • BEAM Instructions: A Brief BEAM Primer
  • Compiler Internals: “The BEAM Book” Chapter 4 - Erik Stenman

Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Completed Phase 3

Real world outcome:

$ erlc +debug_info example.erl
$ erl
1> c(beam_analyzer).
{ok,beam_analyzer}

2> beam_analyzer:analyze("example.beam").
Module: example
Exported functions: 5
Total BEAM instructions: 847

Functions by size:
  example:process_data/2   - 234 instructions
  example:parse_input/1    - 156 instructions
  example:main/1           - 98 instructions
  ...

Tail-call optimized: 12/15 recursive functions
Non-tail recursive (potential stack growth):
  - example:build_tree/1 at line 45

Call graph:
  main/1 -> parse_input/1 -> validate/1
         -> process_data/2 -> transform/1
                           -> aggregate/2

Pattern match complexity:
  example:handle_event/2 - 23 clauses (consider refactoring)

Implementation Hints: Reading BEAM files:

analyze(BeamFile) ->
    {ok, {Module, Chunks}} = beam_lib:chunks(BeamFile, [
        abstract_code,
        atoms,
        indexed_imports,
        exports
    ]),
    ...

For disassembly, compile with debug_info and extract abstract code:

{ok, {_, [{abstract_code, {_, Forms}}]}} =
    beam_lib:chunks(BeamFile, [abstract_code]).
%% Forms is the Erlang Abstract Format (AST)

To get actual BEAM instructions, you need to dig deeper:

%% Get the Code chunk (raw bytecode)
{ok, Bin} = file:read_file(BeamFile),
%% Parse the BEAM file format manually, or use erts_debug:df(Module)

Learning milestones:

  1. Extract basic info from BEAM files → beam_lib understood
  2. Disassemble to instruction level → BEAM format understood
  3. Generate useful analysis → You understand BEAM optimization

Project 12: Custom BEAM Tracer

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (BEAM-specific)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model (B2B Utility)
  • Difficulty: Level 4: Expert (The Systems Architect)
  • Knowledge Area: Debugging / Observability
  • Software or Tool: BEAM Tracing
  • Main Book: “Erlang Programming” by Cesarini & Thompson

What you’ll build: A tracing framework that wraps Erlang’s tracing capabilities (erlang:trace/3, dbg) into a user-friendly tool for debugging production systems: filter by module, function, process; output to file or live; calculate call timing.

Why it teaches Erlang: BEAM’s tracing is legendary—you can trace anything in a live production system with minimal overhead. Understanding tracing is essential for Erlang debugging mastery.

Core challenges you’ll face:

  • Trace flags (calls, messages, timestamps) → maps to erlang:trace/3
  • Match specifications (filtering what to trace) → maps to dbg:fun2ms/1
  • Trace message handling (receiving and formatting) → maps to trace handler processes
  • Performance (not overwhelming the tracer) → maps to rate limiting
  • Output formatting (human-readable vs machine-parseable) → maps to log formatting

Key Concepts:

  • erlang:trace/3: Erlang Documentation
  • Match Specifications: “Erlang Programming” Chapter 17 - Cesarini & Thompson
  • dbg Module: “Learn You Some Erlang” Appendix A - Fred Hébert
  • Recon Library: recon library documentation

Difficulty: Expert Time estimate: 2 weeks Prerequisites: Completed Project 11

Real world outcome:

$ erl
1> my_tracer:start().
ok

2> % Trace all calls to lists:reverse with timing
2> my_tracer:trace({lists, reverse, '_'}, [calls, timing]).
Tracing calls to lists:reverse/_

3> lists:reverse([1,2,3]).
[TRACE] lists:reverse([1,2,3]) -> [3,2,1] (12 µs)
[3,2,1]

4> % Trace messages to a specific process
4> my_tracer:trace_messages(whereis(my_server)).
Tracing messages to <0.85.0>

5> my_server:call(hello).
[MSG] <0.85.0> <- {call, <0.45.0>, hello}
[MSG] <0.45.0> <- {reply, world}
world

6> % Output to file
6> my_tracer:output_file("trace.log").
7> % ... do operations ...
8> my_tracer:stop().
Wrote 1247 trace events to trace.log

Implementation Hints: Setting up a trace:

trace_calls(MFA, Opts) ->
    %% Start tracer process to receive trace messages
    TracerPid = spawn(fun() -> trace_handler([]) end),

    %% Set up trace on the target
    erlang:trace(all, true, [call, {tracer, TracerPid}]),

    %% Set trace pattern
    erlang:trace_pattern(MFA, [{'_', [], [{return_trace}]}], [local]).

The trace handler receives structured messages:

trace_handler(Acc) ->
    receive
        {trace, Pid, call, {M, F, Args}} ->
            io:format("[CALL] ~p:~p(~p)~n", [M, F, Args]),
            trace_handler(Acc);
        {trace, Pid, return_from, {M, F, Arity}, RetVal} ->
            io:format("[RET]  ~p:~p/~p -> ~p~n", [M, F, Arity, RetVal]),
            trace_handler(Acc);
        stop ->
            ok
    end.

Learning milestones:

  1. Basic call tracing works → erlang:trace understood
  2. Match specs filter correctly → Advanced tracing works
  3. Minimal overhead on traced system → Production-ready

Project 13: Process Heap Inspector

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (BEAM-specific)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 1. The “Resume Gold” (Educational/Personal Brand)
  • Difficulty: Level 4: Expert (The Systems Architect)
  • Knowledge Area: Memory Management / VM Internals
  • Software or Tool: BEAM Memory Model
  • Main Book: “The BEAM Book” by Erik Stenman

What you’ll build: A tool that inspects the internal heap structure of Erlang processes, showing term sizes, binary references, garbage collection stats, and identifying memory leaks (large mailboxes, binary accumulation).

Why it teaches Erlang: Understanding BEAM’s per-process GC and memory model is crucial for production Erlang. This project teaches you to diagnose memory issues that are invisible at the application level.

Core challenges you’ll face:

  • Process info extraction → maps to erlang:process_info/2
  • Term size calculation → maps to erts_debug:size/1
  • Binary reference tracking → maps to reference-counted binaries
  • Heap fragmentation analysis → maps to GC internals
  • Visualization → maps to report formatting

Key Concepts:

  • BEAM Memory Model: “The BEAM Book” Chapter 5 - Erik Stenman
  • Process Info: Erlang Documentation - erlang:process_info/2
  • Binary Handling: “Erlang Efficiency Guide” - erlang.org
  • GC Internals: “The BEAM Book” Chapter 6 - Erik Stenman

Difficulty: Expert Time estimate: 2 weeks Prerequisites: Completed Project 11

Real world outcome:

$ erl
1> {ok, Pid} = leaky_server:start().
{ok,<0.85.0>}

2> % Let it run for a while...
2> timer:sleep(10000).

3> heap_inspector:analyze(Pid).
Process: <0.85.0> (leaky_server)
======================================
Heap size: 45.2 MB (WARNING: Large heap)
Stack size: 1.2 KB
Message queue: 0 messages

Memory breakdown:
  Lists: 2.1 MB (12,847 elements total)
  Tuples: 1.4 MB (8,234 tuples)
  Binaries: 41.5 MB (WARNING: Binary accumulation detected)
    - Ref-counted binaries: 41.3 MB across 847 refs
    - Heap binaries: 0.2 MB

Large terms detected:
  - List at depth 3: 1.8 MB
  - Binary reference: 5.2 MB (held since 847 GC cycles)

GC stats:
  Minor GCs: 1247
  Major GCs (fullsweep): 3
  Fullsweep after: 65535 (PROBLEM: too high, binaries not freed)

DIAGNOSIS: This process is accumulating binary references.
Possible causes:
  - Sub-binary references keeping large binaries alive
  - Missing explicit binary GC

Recommendation: Lower fullsweep_after to 100 or call
erlang:garbage_collect(Pid) explicitly.

Implementation Hints: Key process_info fields:

analyze(Pid) ->
    Info = erlang:process_info(Pid, [
        heap_size,
        stack_size,
        message_queue_len,
        garbage_collection,
        binary,
        current_function,
        dictionary
    ]),

    %% binary field shows all ref-counted binaries the process holds
    {binary, Binaries} = lists:keyfind(binary, 1, Info),
    %% Each is {BinaryId, Size, RefCount}
    TotalBinaryMem = lists:sum([Size || {_Id, Size, _Refs} <- Binaries]),
    ...

Getting term sizes:

%% This requires erlang to be compiled with debug info
term_size(Term) ->
    erts_debug:flat_size(Term).  % Words used

Learning milestones:

  1. Extract basic heap stats → process_info understood
  2. Identify binary accumulation → Common leak pattern detected
  3. Provide actionable recommendations → Production debugging mastery

Project 14: Scheduler Utilization Monitor

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (BEAM-specific)
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model (B2B Utility)
  • Difficulty: Level 4: Expert (The Systems Architect)
  • Knowledge Area: VM Internals / Performance
  • Software or Tool: BEAM Schedulers
  • Main Book: “The BEAM Book” by Erik Stenman

What you’ll build: A real-time dashboard showing BEAM scheduler utilization, run queue lengths, reduction counts, and identifying processes that are hogging schedulers or causing imbalanced load.

Why it teaches Erlang: BEAM’s scheduler is what makes Erlang magic—preemptive scheduling via reductions, work-stealing across cores, dirty schedulers for NIFs. Understanding scheduler behavior is key to performance.

Core challenges you’ll face:

  • Scheduler stats → maps to erlang:statistics/1
  • Run queue monitoring → maps to scheduler run queues
  • Reduction tracking → maps to understanding reductions
  • Load balancing detection → maps to work stealing
  • Real-time updates → maps to periodic sampling

Key Concepts:

  • Schedulers: “The BEAM Book” Chapter 7 - Erik Stenman
  • Statistics: Erlang Documentation - erlang:statistics/1
  • Reductions: “Erlang Efficiency Guide” - erlang.org
  • Performance Analysis: “Erlang Programming” Chapter 19 - Cesarini & Thompson

Difficulty: Expert Time estimate: 1-2 weeks Prerequisites: Completed Project 12

Real world outcome:

$ erl +S 8 +SDcpu 8
1> scheduler_monitor:start().

╔════════════════════════════════════════════════════════════════╗
║               BEAM Scheduler Monitor (8 schedulers)             ║
╠════════════════════════════════════════════════════════════════╣
║ Scheduler │ Utilization │ Run Queue │ Reductions/s │ Status    ║
╠═══════════╪═════════════╪═══════════╪══════════════╪═══════════╣
║     1     │ ████████ 87%│     3     │   1,234,567  │ HOT       ║
║     2     │ ███░░░░░ 35%│     0     │     456,789  │ normal    ║
║     3     │ ██░░░░░░ 22%│     0     │     234,567  │ normal    ║
║     4     │ ██░░░░░░ 19%│     0     │     198,765  │ normal    ║
║     5     │ █░░░░░░░ 12%│     0     │      98,765  │ idle      ║
║     6     │ █░░░░░░░ 11%│     0     │      87,654  │ idle      ║
║     7     │ █░░░░░░░ 10%│     0     │      76,543  │ idle      ║
║     8     │ █░░░░░░░  9%│     0     │      65,432  │ idle      ║
╠════════════════════════════════════════════════════════════════╣
║ WARNING: Scheduler 1 overloaded! Top processes:                ║
║   <0.85.0> (worker) - 45% of scheduler 1 reductions            ║
║   <0.92.0> (parser) - 23% of scheduler 1 reductions            ║
║ Consider: Process migration or parallelization                 ║
╚════════════════════════════════════════════════════════════════╝

Implementation Hints: Getting scheduler stats:

get_scheduler_utilization() ->
    %% Must call wall_clock before and after to calculate utilization
    erlang:statistics(scheduler_wall_time),
    timer:sleep(1000),
    SchedTimes = erlang:statistics(scheduler_wall_time),
    %% Returns: [{SchedId, ActiveTime, TotalTime}, ...]
    [{Id, Active/Total * 100} || {Id, Active, Total} <- SchedTimes].

Run queue lengths:

get_run_queues() ->
    erlang:statistics(run_queue_lengths).
    %% Returns: [Q1, Q2, Q3, ...] for each scheduler

Finding reduction hogs:

top_processes(N) ->
    Procs = erlang:processes(),
    WithReds = [{P, element(2, erlang:process_info(P, reductions))} || P <- Procs],
    lists:sublist(lists:reverse(lists:keysort(2, WithReds)), N).

Learning milestones:

  1. Show scheduler utilization → Basic stats understood
  2. Detect overloaded schedulers → Load analysis works
  3. Correlate with specific processes → Full observability achieved

PHASE 5: ADVANCED & INTEGRATION


Project 15: Erlang NIF for Fast JSON

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang + C
  • Alternative Programming Languages: Erlang + Rust, Erlang + Zig
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model (B2B Utility)
  • Difficulty: Level 4: Expert (The Systems Architect)
  • Knowledge Area: FFI / Native Code
  • Software or Tool: NIFs (Native Implemented Functions)
  • Main Book: “The BEAM Book” by Erik Stenman

What you’ll build: A NIF (Native Implemented Function) that wraps a fast C JSON parser (like yyjson or simdjson) to provide high-performance JSON parsing for Erlang, learning the NIF API along the way.

Why it teaches Erlang: NIFs are how Erlang integrates with native code. You’ll learn the NIF API, resource objects, dirty schedulers (for long-running NIFs), and the tradeoffs of native code in BEAM.

Core challenges you’ll face:

  • NIF boilerplate (module setup, function registration) → maps to NIF API
  • Type conversion (Erlang terms ↔ C types) → maps to enif_ functions*
  • Memory management (who owns what) → maps to resource objects
  • Scheduler safety (don’t block schedulers) → maps to dirty NIFs
  • Error handling (C errors → Erlang exceptions) → maps to enif_raise_exception

Key Concepts:

  • NIF Tutorial: Erlang Documentation - “How to write a NIF”
  • NIF API: “The BEAM Book” Chapter 11 - Erik Stenman
  • Dirty NIFs: Erlang Documentation - “Dirty NIFs”
  • Resource Objects: “The BEAM Book” Chapter 11.4 - Erik Stenman

Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: C programming, completed Phase 3

Real world outcome:

$ make
Compiling json_nif.c...
Compiling Erlang wrapper...
Done.

$ erl -pa ebin
1> json_nif:parse(<<"{\"name\": \"Erlang\", \"version\": 26}">>).
{ok, #{<<"name">> => <<"Erlang">>, <<"version">> => 26}}

2> % Benchmark against pure Erlang JSON
2> big_json = file:read_file("big.json").
3> timer:tc(fun() -> json_nif:parse(big_json) end).
{1234, {ok, ...}}  % 1.2 ms with NIF

4> timer:tc(fun() -> jsx:decode(big_json) end).
{15678, ...}  % 15.6 ms with pure Erlang

5> % NIF is 12x faster!

Implementation Hints: NIF skeleton in C:

#include "erl_nif.h"
#include "yyjson.h"

static ERL_NIF_TERM parse_nif(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    ErlNifBinary json_bin;
    if (!enif_inspect_binary(env, argv[0], &json_bin)) {
        return enif_make_badarg(env);
    }

    yyjson_doc *doc = yyjson_read((char*)json_bin.data, json_bin.size, 0);
    if (!doc) {
        return enif_make_tuple2(env,
            enif_make_atom(env, "error"),
            enif_make_atom(env, "parse_error"));
    }

    ERL_NIF_TERM result = convert_to_term(env, yyjson_doc_get_root(doc));
    yyjson_doc_free(doc);

    return enif_make_tuple2(env, enif_make_atom(env, "ok"), result);
}

static ErlNifFunc nif_funcs[] = {
    {"parse", 1, parse_nif, ERL_NIF_DIRTY_JOB_CPU_BOUND}
};

ERL_NIF_INIT(json_nif, nif_funcs, NULL, NULL, NULL, NULL)

Note ERL_NIF_DIRTY_JOB_CPU_BOUND - this runs on a dirty scheduler so it doesn’t block regular schedulers.

Learning milestones:

  1. Basic NIF compiles and loads → NIF infrastructure understood
  2. Simple types convert correctly → Type conversion works
  3. Complex JSON parses correctly → Full implementation works
  4. Benchmarks show significant speedup → NIF was worth it

Project 16: Hot Code Upgrade System

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (Erlang-specific)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure (Enterprise Scale)
  • Difficulty: Level 5: Master (The First-Principles Wizard)
  • Knowledge Area: Release Engineering / VM Internals
  • Software or Tool: OTP Releases
  • Main Book: “Erlang and OTP in Action” by Logan, Merritt, Carlsson

What you’ll build: A complete hot code upgrade system for an Erlang application: release packaging, appup files, state transformation during upgrade, and rollback capability—upgrading a running chat server without dropping connections.

Why it teaches Erlang: Hot code loading is Erlang’s killer feature. This project teaches you the full lifecycle: code_change callbacks, appup/relup files, release handling, and the legendary “upgrade without downtime.”

Core challenges you’ll face:

  • Release packaging → maps to rebar3 releases
  • Appup files (upgrade instructions) → maps to OTP upgrade mechanism
  • code_change callback (state transformation) → maps to GenServer upgrades
  • Relup generation → maps to systools
  • Rollback → maps to release_handler

Key Concepts:

  • Releases: “Erlang and OTP in Action” Chapter 10 - Logan et al.
  • Appup Files: Erlang Documentation - “Creating .appup Files”
  • code_change: “Designing for Scalability” Chapter 10 - Cesarini & Vinoski
  • Release Handler: Erlang Documentation - “release_handler”

Difficulty: Master Time estimate: 3-4 weeks Prerequisites: Completed Phase 4

Real world outcome:

# Terminal 1 - Running chat server v1.0.0
$ _build/default/rel/chat/bin/chat foreground
Chat server v1.0.0 starting...
[INFO] Client alice connected
[INFO] Client bob connected

# Terminal 2 - Client alice
$ telnet localhost 4000
Connected to Chat v1.0.0
> Hello from v1!

# Terminal 3 - Deploy upgrade to v1.1.0 (adds timestamps)
$ rebar3 release
$ rebar3 appup generate
$ rebar3 relup
$ _build/default/rel/chat/bin/chat upgrade "1.1.0"
Release upgraded from 1.0.0 to 1.1.0

# Terminal 1 now shows:
[INFO] Upgraded to v1.1.0
[INFO] State migrated for 2 active connections

# Terminal 2 - alice still connected!
> Hello from v1.1!
[12:34:56] alice: Hello from v1.1!  <- New timestamp feature!

# Rollback if needed
$ _build/default/rel/chat/bin/chat downgrade "1.0.0"

Implementation Hints: The code_change callback transforms state:

%% In chat_server.erl (gen_server)
code_change({down, "1.0.0"}, State, _Extra) ->
    %% Downgrading from 1.1.0 to 1.0.0
    %% Remove timestamp from state
    NewState = maps:remove(timestamp_enabled, State),
    {ok, NewState};
code_change("1.0.0", State, _Extra) ->
    %% Upgrading from 1.0.0 to 1.1.0
    %% Add timestamp feature
    NewState = State#{timestamp_enabled => true},
    {ok, NewState}.

The appup file (chat.appup):

{"1.1.0",
 [{"1.0.0", [
    {update, chat_server, {advanced, []}}
  ]}],
 [{"1.0.0", [
    {update, chat_server, {advanced, []}}
  ]}]
}.

Learning milestones:

  1. Release packages correctly → Release basics understood
  2. Simple upgrade works → Appup understood
  3. State transforms correctly → code_change works
  4. Connections survive upgrade → Zero-downtime achieved!

Project 17: Parse Transform Macro System

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (Erlang-specific)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 1. The “Resume Gold” (Educational/Personal Brand)
  • Difficulty: Level 5: Master (The First-Principles Wizard)
  • Knowledge Area: Metaprogramming / Compiler
  • Software or Tool: Parse Transforms
  • Main Book: “Metaprogramming Elixir” by Chris McCord (for concepts, then apply to Erlang)

What you’ll build: A parse transform that adds Elixir-like pipe operator (|>) to Erlang, transforming X |> foo() |> bar(Y) into bar(foo(X), Y) at compile time.

Why it teaches Erlang: Parse transforms are Erlang’s metaprogramming system—more powerful than Elixir macros but also more dangerous. This teaches you the Erlang Abstract Format (AST) and compile-time code transformation.

Core challenges you’ll face:

  • Erlang Abstract Format (the AST) → maps to understanding term structure
  • Parse transform interface → maps to parse_transform/2 function
  • AST walking (find all |> operators) → maps to recursive tree traversal
  • AST rewriting (transform to function calls) → maps to term construction
  • Error reporting (good compile errors) → maps to compiler integration

Key Concepts:

  • Parse Transforms: Erlang Documentation - “Parse Transformations”
  • Erlang Abstract Format: Erlang Documentation - “The Abstract Format”
  • AST Manipulation: “Language Implementation Patterns” Chapter 5 - Terence Parr
  • Compile Module: Erlang Documentation - “compile”

Difficulty: Master Time estimate: 2-3 weeks Prerequisites: Completed Phase 4

Real world outcome:

%% In my_module.erl
-module(my_module).
-compile({parse_transform, pipe_transform}).
-export([example/0]).

example() ->
    "hello world"
    |> string:uppercase()
    |> string:split(" ")
    |> lists:reverse()
    |> lists:join("-").

%% Compiles to:
%% lists:join("-", lists:reverse(string:split(string:uppercase("hello world"), " "))).
$ erlc pipe_transform.erl
$ erlc my_module.erl
$ erl
1> my_module:example().
"WORLD-HELLO"

Implementation Hints: Parse transform skeleton:

-module(pipe_transform).
-export([parse_transform/2]).

parse_transform(Forms, _Options) ->
    [transform_form(Form) || Form <- Forms].

transform_form({function, Line, Name, Arity, Clauses}) ->
    {function, Line, Name, Arity, [transform_clause(C) || C <- Clauses]};
transform_form(Other) ->
    Other.

transform_clause({clause, Line, Patterns, Guards, Body}) ->
    {clause, Line, Patterns, Guards, [transform_expr(E) || E <- Body]}.

%% Find the pipe operator
transform_expr({op, Line, '|>', Left, Right}) ->
    %% Transform: Left |> Right into Right(Left) or Right(Left, Args...)
    insert_as_first_arg(transform_expr(Left), transform_expr(Right));
transform_expr({call, Line, Fun, Args}) ->
    {call, Line, Fun, [transform_expr(A) || A <- Args]};
transform_expr(Other) ->
    Other.

insert_as_first_arg(Arg, {call, Line, Fun, Args}) ->
    {call, Line, Fun, [Arg | Args]}.

The tricky part is handling the AST format correctly. Use erl_syntax module for easier manipulation:

%% Alternatively, use erl_syntax for cleaner code
transform_expr(Expr) ->
    erl_syntax_lib:map(fun transform_node/1, Expr).

Learning milestones:

  1. Parse transform compiles → Basic structure works
  2. Simple pipes transform → AST rewriting works
  3. Nested pipes work → Recursive transformation correct
  4. Good error messages on invalid input → Production quality

Project 18: Distributed Erlang Game Server

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: Elixir, Go
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure (Enterprise Scale)
  • Difficulty: Level 4: Expert (The Systems Architect)
  • Knowledge Area: Distributed Systems / Game Development
  • Software or Tool: Distributed Erlang
  • Main Book: “Designing for Scalability with Erlang/OTP” by Cesarini & Vinoski

What you’ll build: A multiplayer game server (simple real-time game like tag or capture-the-flag) running across multiple Erlang nodes, with automatic player handoff when servers fail, using global and pg for process discovery.

Why it teaches Erlang: Games are the ultimate test of Erlang’s strengths: massive concurrency (player per process), real-time requirements, fault tolerance (server crash shouldn’t end the game), and distribution.

Core challenges you’ll face:

  • Player process per connection → maps to massive concurrency
  • Game world state (positions, collisions) → maps to shared state patterns
  • Node clustering → maps to distributed Erlang
  • Process migration (when node fails) → maps to process handoff
  • Real-time updates (60 updates/second) → maps to timer:send_interval
  • Client protocol (WebSocket for browser) → maps to cowboy websocket

Key Concepts:

  • Distributed Erlang: “Programming Erlang” Chapter 10 - Joe Armstrong
  • Global Registration: Erlang Documentation - “global”
  • Process Groups: Erlang Documentation - “pg”
  • Cowboy Websockets: Cowboy documentation
  • Game Loops: Game programming resources

Difficulty: Expert Time estimate: 4-6 weeks Prerequisites: Completed all previous phases

Real world outcome:

# Start 3 game server nodes
$ ./start_node.sh node1 8080
$ ./start_node.sh node2 8081
$ ./start_node.sh node3 8082

# Open browser to http://localhost:8080/game
# See: "Connected to node1. 15 players online."
# Move your character with arrow keys
# See other players moving in real-time

# Kill node1 (Ctrl+C)
# Browser automatically reconnects to node2
# Game continues seamlessly!
# "Reconnected to node2. 15 players online."

# Start node1 again
# Some players automatically migrate back to balance load

Implementation Hints: Architecture:

                    ┌─────────────────────────────────────────┐
                    │             Load Balancer               │
                    └──────────────┬──────────────────────────┘
                                   │
         ┌─────────────────────────┼─────────────────────────┐
         │                         │                         │
    ┌────▼────┐              ┌─────▼────┐              ┌─────▼────┐
    │  Node1  │◄────────────►│  Node2   │◄────────────►│  Node3   │
    │(Erlang) │  Distributed │ (Erlang) │  Erlang     │ (Erlang) │
    └────┬────┘   Erlang     └─────┬────┘              └─────┬────┘
         │                         │                         │
   ┌─────┼─────┐             ┌─────┼─────┐             ┌─────┼─────┐
   │Player│Game│             │Player│Game│             │Player│Game│
   │Procs │Loop│             │Procs │Loop│             │Procs │Loop│
   └──────┴────┘             └──────┴────┘             └──────┴────┘

Key pattern - player as process with mailbox:

player_loop(State = #{socket := Socket, position := Pos}) ->
    receive
        {move, Direction} ->
            NewPos = calculate_new_position(Pos, Direction),
            broadcast_position(State#{position := NewPos}),
            player_loop(State#{position := NewPos});
        {world_update, WorldState} ->
            send_to_client(Socket, encode_world(WorldState)),
            player_loop(State);
        {node_shutdown, NewNode} ->
            %% Migrate to new node
            migrate_to(NewNode, State)
    after 16 ->  % ~60 FPS
        player_loop(State)
    end.

Using pg for process groups:

%% All players in a game room
pg:join(game_room_1, self()),

%% Broadcast to all players in room
[Pid ! {world_update, State} || Pid <- pg:get_members(game_room_1)].

Learning milestones:

  1. Single-node game works → Basic architecture correct
  2. Multi-node clustering works → Distribution works
  3. Player survives node failure → Fault tolerance achieved
  4. Smooth gameplay at 60 FPS → Performance tuned

CAPSTONE PROJECT


Project 19: Production-Ready Message Queue (Mini-RabbitMQ)

  • File: ERLANG_FROM_FIRST_PRINCIPLES_PROJECTS.md
  • Main Programming Language: Erlang
  • Alternative Programming Languages: None (this should be pure Erlang)
  • Coolness Level: Level 5: Pure Magic (Super Cool)
  • Business Potential: 4. The “Open Core” Infrastructure (Enterprise Scale)
  • Difficulty: Level 5: Master (The First-Principles Wizard)
  • Knowledge Area: Distributed Systems / Message Queues
  • Software or Tool: RabbitMQ (as inspiration)
  • Main Book: “RabbitMQ in Action” by Videla & Williams (for concepts)

What you’ll build: A simplified but production-quality message queue system with: exchanges, queues, bindings, persistent messages (using Mnesia), consumer acknowledgments, clustering, and an admin API.

Why this is the capstone: This project integrates everything: OTP behaviors (gen_server, supervisor, gen_statem), Mnesia for persistence, distributed Erlang for clustering, NIFs potentially for performance, hot code loading for upgrades, and tracing for debugging. This is why Erlang was invented.

Core challenges you’ll face:

  • Exchange/Queue/Binding model → maps to domain modeling
  • Message persistence → maps to Mnesia transactions
  • Consumer management → maps to process monitoring
  • Acknowledgment tracking → maps to state machines
  • Clustering → maps to distributed Erlang + Mnesia
  • Backpressure → maps to flow control
  • Admin API → maps to Cowboy REST
  • Metrics → maps to counters + observers

Key Concepts:

  • AMQP Model: “RabbitMQ in Action” Chapter 2 - Videla & Williams
  • Persistence: “Erlang and OTP in Action” Chapter 6 - Logan et al.
  • Flow Control: “Designing for Scalability” Chapter 12 - Cesarini & Vinoski
  • Distributed Mnesia: Erlang Documentation - “Mnesia User’s Guide”

Difficulty: Master Time estimate: 2-3 months Prerequisites: Completed all 18 projects above

Real world outcome:

# Start a 3-node cluster
$ ./emq start node1 5672
$ ./emq join node2 5673 node1
$ ./emq join node3 5674 node1

# CLI management
$ ./emq-admin status
Cluster: 3 nodes
  node1@localhost: running, 12 queues, 45MB memory
  node2@localhost: running, 8 queues, 32MB memory
  node3@localhost: running, 10 queues, 38MB memory

$ ./emq-admin declare-exchange events topic
$ ./emq-admin declare-queue user-events --durable
$ ./emq-admin bind user-events events "user.*"

# Producer (in Erlang shell)
1> {ok, Conn} = emq_client:connect("localhost", 5672).
2> emq_client:publish(Conn, "events", "user.created", <<"User 123 created">>).
ok

# Consumer (separate shell)
1> {ok, Conn} = emq_client:connect("localhost", 5672).
2> emq_client:subscribe(Conn, "user-events", fun(Msg) ->
     io:format("Got: ~p~n", [Msg]),
     ack
   end).

Got: {message, <<"user.created">>, <<"User 123 created">>}

# Kill node2
$ ./emq stop node2

# Messages automatically route through remaining nodes
# Queues on node2 failover to node1 and node3

Implementation Hints: This project should use all the patterns you’ve learned:

Supervision tree:

                        ┌──────────────────┐
                        │   emq_sup        │
                        │ (top supervisor) │
                        └────────┬─────────┘
           ┌────────────────────┬┴────────────────────┐
           ▼                    ▼                     ▼
    ┌──────────────┐    ┌──────────────┐     ┌───────────────┐
    │ exchange_sup │    │  queue_sup   │     │ connection_sup│
    │(one_for_one) │    │(simple_1_1)  │     │ (simple_1_1)  │
    └──────────────┘    └──────────────┘     └───────────────┘

Message flow (as gen_statem):

    PUBLISH                   ROUTE                    DELIVER
 ┌───────────┐          ┌──────────────┐          ┌────────────┐
 │ Exchange  │ ────────►│   Queue      │ ────────►│  Consumer  │
 │ (gen_srv) │ bindings │ (gen_statem) │ msgs     │ (gen_srv)  │
 └───────────┘          └──────────────┘          └────────────┘
                              │
                              ▼ persist
                        ┌──────────┐
                        │  Mnesia  │
                        └──────────┘

Queue states (gen_statem):

-type state() :: empty | has_messages | has_consumers | ready.

empty(enter, _, Data) -> {keep_state, Data};
empty({call, From}, {publish, Msg}, Data) ->
    persist(Msg, Data),
    {next_state, has_messages, add_msg(Msg, Data), [{reply, From, ok}]}.

has_messages(enter, _, Data) ->
    %% Try to deliver to consumers
    try_deliver(Data);
has_messages({call, From}, {consume, Pid}, Data) ->
    {next_state, ready, add_consumer(Pid, Data), [{reply, From, ok}]}.

Learning milestones:

  1. Single-node pub/sub works → Basic architecture done
  2. Persistence survives restart → Mnesia integration works
  3. Clustering works → Distributed Erlang mastered
  4. Handles 10K messages/second → Performance acceptable
  5. Node failure handled gracefully → Production-ready

Project Comparison Table

Project Difficulty Time Depth of Understanding Fun Factor
1. Calculator REPL Beginner Weekend ★★☆☆☆ ★★☆☆☆
2. INI Parser Beginner Weekend ★★☆☆☆ ★★☆☆☆
3. Markdown→HTML Intermediate 1-2 weeks ★★★☆☆ ★★★☆☆
4. Chat Server (no OTP) Intermediate 1-2 weeks ★★★★☆ ★★★★☆
5. Process Pool Advanced 1-2 weeks ★★★★☆ ★★★☆☆
6. Rate Limiter Intermediate 1 week ★★★☆☆ ★★★☆☆
7. Build GenServer Advanced 1-2 weeks ★★★★★ ★★★★☆
8. Supervision Visualizer Advanced 1-2 weeks ★★★★☆ ★★★☆☆
9. Mnesia KV Store Expert 2-3 weeks ★★★★★ ★★★★☆
10. gen_statem Protocol Advanced 2 weeks ★★★★☆ ★★★☆☆
11. BEAM Analyzer Expert 2-3 weeks ★★★★★ ★★★★★
12. Custom Tracer Expert 2 weeks ★★★★★ ★★★★☆
13. Heap Inspector Expert 2 weeks ★★★★★ ★★★★☆
14. Scheduler Monitor Expert 1-2 weeks ★★★★★ ★★★★☆
15. JSON NIF Expert 2-3 weeks ★★★★☆ ★★★★☆
16. Hot Code Upgrade Master 3-4 weeks ★★★★★ ★★★★★
17. Parse Transform Master 2-3 weeks ★★★★★ ★★★★★
18. Distributed Game Expert 4-6 weeks ★★★★★ ★★★★★
19. Message Queue Master 2-3 months ★★★★★ ★★★★★

Recommendation

Given your Elixir background, I recommend this order:

Week 1-2: Syntax Transition

Start with Project 1 (Calculator REPL) and Project 2 (INI Parser). These are fast and will cement Erlang syntax in your muscle memory. The goal is to stop thinking “how do I write this in Erlang?” and just write it.

Week 3-4: Concurrency Without Training Wheels

Jump to Project 4 (Chat Server without OTP). This is the “aha!” project. You know how GenServer works in Elixir—now build it from raw processes. When you’re done, you’ll truly understand what OTP provides.

Week 5-6: OTP Mastery

Do Project 7 (Build Your Own GenServer). This demystifies OTP completely. Then Project 8 (Supervision Visualizer) to understand production system introspection.

After that: Follow your interests

  • Want to understand BEAM deeply? → Projects 11-14
  • Want to build distributed systems? → Projects 9, 18
  • Want to push Erlang’s limits? → Projects 15-17
  • Want the full journey? → Do them all, ending with Project 19

Essential Resources

Books (in learning order)

  1. “Learn You Some Erlang for Great Good!” by Fred Hébert - Free online, humorous, excellent for syntax
  2. “Programming Erlang” by Joe Armstrong - Written by Erlang’s creator, the definitive intro
    • Chapters 1-10 for basics, 11-20 for OTP
  3. “Erlang and OTP in Action” by Logan, Merritt, Carlsson - Production patterns and best practices
  4. “Designing for Scalability with Erlang/OTP” by Cesarini & Vinoski - Advanced OTP design
    • When you’re ready to build serious systems
  5. “The BEAM Book” by Erik Stenman - VM internals

Online Resources


Summary

# Project Main Language
1 Erlang Calculator REPL Erlang
2 INI File Parser & Writer Erlang
3 Markdown to HTML Converter Erlang
4 Chat Server Without OTP Erlang
5 Process Pool Manager Erlang
6 Rate Limiter with Token Bucket Erlang
7 Build Your Own GenServer Erlang
8 Supervision Tree Visualizer Erlang
9 Distributed Key-Value Store with Mnesia Erlang
10 State Machine with gen_statem Erlang
11 BEAM Bytecode Analyzer Erlang
12 Custom BEAM Tracer Erlang
13 Process Heap Inspector Erlang
14 Scheduler Utilization Monitor Erlang
15 Erlang NIF for Fast JSON Erlang + C
16 Hot Code Upgrade System Erlang
17 Parse Transform Macro System Erlang
18 Distributed Erlang Game Server Erlang
19 Production-Ready Message Queue (Mini-RabbitMQ) Erlang

By the end of these projects, you won’t just “know Erlang”—you’ll understand why Erlang exists, what problems it uniquely solves, and how to build systems that run forever.