← Back to all projects

LEARN PHOENIX FRAMEWORK DEEP DIVE

Learn Phoenix Framework: From BEAM Fundamentals to Real-Time Mastery

Goal: Deeply understand Phoenix—not just how to use it, but how it works internally, why it’s so fast, what makes LiveView magical, and how it compares to other web frameworks. You’ll build everything from a raw TCP server to a full real-time application, understanding each layer.


Why Phoenix Matters

Phoenix isn’t just another web framework. It’s built on Elixir, which runs on the BEAM (Erlang Virtual Machine)—the same technology that powers WhatsApp (handling 2 million connections per server), Discord (handling 5 million concurrent users), and telecom systems with 99.9999999% uptime.

After completing these projects, you will:

  • Understand the BEAM’s actor model and why it enables massive concurrency
  • Know how Phoenix handles millions of WebSocket connections
  • Understand how LiveView creates reactive UIs without JavaScript
  • See how Plug middleware composes the request pipeline
  • Master Ecto’s functional approach to database interactions
  • Compare Phoenix to Rails, Django, Express, and Go frameworks
  • Build real-time applications that scale horizontally

Core Concept Analysis

The Technology Stack

┌─────────────────────────────────────────────────────────────────────────┐
│                         YOUR APPLICATION                                │
├─────────────────────────────────────────────────────────────────────────┤
│                     PHOENIX FRAMEWORK                                   │
│        (Router, Controllers, Views, Channels, LiveView)                 │
├─────────────────────────────────────────────────────────────────────────┤
│                          PLUG                                           │
│         (Composable middleware, Conn struct)                            │
├─────────────────────────────────────────────────────────────────────────┤
│                          ECTO                                           │
│      (Database toolkit: Repos, Schemas, Changesets, Query)              │
├─────────────────────────────────────────────────────────────────────────┤
│                     COWBOY HTTP SERVER                                  │
│            (Erlang HTTP/1.1, HTTP/2, WebSocket server)                  │
├─────────────────────────────────────────────────────────────────────────┤
│                    ELIXIR LANGUAGE                                      │
│   (Functional, immutable, pattern matching, metaprogramming)            │
├─────────────────────────────────────────────────────────────────────────┤
│                    OTP (Open Telecom Platform)                          │
│     (GenServer, Supervisor, Application - behaviors for concurrency)    │
├─────────────────────────────────────────────────────────────────────────┤
│                    BEAM Virtual Machine                                 │
│   (Lightweight processes, preemptive scheduling, fault tolerance)       │
└─────────────────────────────────────────────────────────────────────────┘

The BEAM’s Secret Sauce: Lightweight Processes

Traditional Web Server (Node.js, Python, Ruby)
──────────────────────────────────────────────

┌───────────────────────────────────────────────────────────┐
│                   Single OS Thread/Process                │
│  ┌─────────────────────────────────────────────────────┐  │
│  │  Request 1 ───► Request 2 ───► Request 3 ───► ...   │  │
│  │     (blocking or callback-based async)               │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                           │
│  Problem: One slow request can affect others              │
│  Problem: Crash in one request can crash everything       │
└───────────────────────────────────────────────────────────┘


BEAM Virtual Machine (Elixir/Phoenix)
─────────────────────────────────────

┌─────────────────────────────────────────────────────────────────────┐
│                      BEAM VM (per CPU core)                         │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │  Scheduler 1        Scheduler 2        Scheduler 3   ...    │   │
│  │      │                  │                  │                │   │
│  │  ┌───┴───┐          ┌───┴───┐          ┌───┴───┐           │   │
│  │  │ P  P  │          │ P  P  │          │ P  P  │           │   │
│  │  │ P  P  │          │ P  P  │          │ P  P  │           │   │
│  │  │ P  P  │          │ P  P  │          │ P  P  │           │   │
│  │  └───────┘          └───────┘          └───────┘           │   │
│  │  (Each P is a lightweight process, ~2KB memory)             │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                     │
│  ✓ Each request gets its own isolated process                      │
│  ✓ Processes are preemptively scheduled (no blocking)              │
│  ✓ Crash in one process doesn't affect others                      │
│  ✓ Millions of processes can run concurrently                      │
└─────────────────────────────────────────────────────────────────────┘

Phoenix Request Flow

HTTP Request
     │
     ▼
┌─────────────┐
│  Endpoint   │  ← First stop: static files, logging, session, CSRF
│  (Plug)     │
└─────┬───────┘
      │
      ▼
┌─────────────┐
│   Router    │  ← Matches URL pattern, selects pipeline and controller
│  (Plug)     │
└─────┬───────┘
      │
      ▼
┌─────────────┐
│  Pipeline   │  ← :browser adds session, flash, etc.
│  (Plugs)    │     :api adds JSON parsing
└─────┬───────┘
      │
      ▼
┌─────────────┐
│ Controller  │  ← Handles business logic, calls context functions
│  (Plug)     │
└─────┬───────┘
      │
      ▼
┌─────────────┐
│   View      │  ← Prepares data for rendering
│             │
└─────┬───────┘
      │
      ▼
┌─────────────┐
│  Template   │  ← Compiled EEx, generates HTML
│  (HEEx)     │
└─────┬───────┘
      │
      ▼
HTTP Response

LiveView: How It Works

Initial Page Load (HTTP)
────────────────────────

Browser ──HTTP GET──► Phoenix ──renders──► Full HTML Page
                                               │
                                               ▼
                                          Browser displays page
                                               │
                                               ▼
                                          JavaScript loads
                                               │
                                               ▼
                                          WebSocket connects
                                               │
                                               ▼
                              ┌────────────────┴────────────────┐
                              │                                 │
                        Phoenix spawns                     Browser ready
                        LiveView process                   for updates
                        (stateful, long-lived)


User Interaction (WebSocket)
────────────────────────────

Browser                                          LiveView Process
   │                                                    │
   │──── phx-click="increment" ────────────────────────►│
   │                                                    │
   │                                          handle_event("increment")
   │                                                    │
   │                                          Updates socket.assigns
   │                                                    │
   │                                          Re-renders template
   │                                                    │
   │                                          Computes DIFF (only changes)
   │                                                    │
   │◄───────── {diff: [{0: "6"}]} ─────────────────────│
   │                                                    │
   │  JavaScript patches DOM                            │
   │  with minimal changes                              │
   ▼                                                    │

Key Insight: Only the changed parts are sent!
- Template has static and dynamic parts
- Phoenix tracks which assigns changed
- Only sends new values for changed dynamics

How Phoenix Compares to Other Frameworks

┌─────────────────────────────────────────────────────────────────────────────┐
│                    CONCURRENCY MODEL COMPARISON                             │
├─────────────────┬───────────────────────────────────────────────────────────┤
│ Framework       │ How it handles 10,000 concurrent connections              │
├─────────────────┼───────────────────────────────────────────────────────────┤
│                 │                                                           │
│ Ruby on Rails   │  Thread pool (typically 5-25 threads)                     │
│                 │  Each request blocks a thread                             │
│                 │  Need load balancer + many processes                      │
│                 │                                                           │
│ Django          │  Similar to Rails (WSGI is synchronous)                   │
│                 │  ASGI + async views help but add complexity               │
│                 │                                                           │
│ Node.js/Express │  Single-threaded event loop                               │
│                 │  Non-blocking I/O, callbacks/promises                     │
│                 │  CPU-bound work blocks everything                         │
│                 │  Need worker threads or cluster mode                      │
│                 │                                                           │
│ Go (net/http)   │  Goroutines (lightweight, like BEAM processes)            │
│                 │  Excellent concurrency, but manual error handling         │
│                 │  No supervision trees                                     │
│                 │                                                           │
│ Phoenix/Elixir  │  10,000 BEAM processes (one per connection)               │
│                 │  Preemptive scheduling, no blocking                       │
│                 │  Supervision trees auto-restart failed processes          │
│                 │  Built for "let it crash" philosophy                      │
│                 │                                                           │
└─────────────────┴───────────────────────────────────────────────────────────┘

Feature Comparison

Feature Phoenix Rails Django Express Go (std lib)
Real-time (WebSockets) Built-in Channels + LiveView ActionCable (bolt-on) Channels (bolt-on) Socket.io (3rd party) gorilla/websocket
Concurrency Model Actor model (millions of processes) Thread pool Thread pool (async optional) Event loop Goroutines
Fault Tolerance Supervisor trees (auto-restart) External (systemd) External External Manual
Hot Code Reload Yes (BEAM feature) No No No No
Database Ecto (functional) ActiveRecord (ORM) Django ORM Choose your own database/sql
Learning Curve Steep (new paradigm) Low Low Low Medium
Community Size Small but growing Very large Very large Massive Large
Performance Excellent Good Good Excellent Excellent
Productivity High (once learned) Very High Very High Medium Medium

When to Choose Phoenix

Phoenix excels at:

  • Real-time features (chat, live updates, notifications)
  • High concurrency (many simultaneous connections)
  • Fault-tolerant systems (financial, telecom)
  • Long-running connections (IoT, streaming)
  • Applications that need to scale horizontally

Consider alternatives when:

  • Team has no functional programming experience and can’t invest in learning
  • Hiring is a concern (smaller developer pool)
  • You need extensive third-party library ecosystem
  • Simple CRUD apps where Rails/Django productivity wins

Project List

Projects are ordered from foundational understanding to advanced implementations.


Project 1: Build a TCP Echo Server (Understanding BEAM Processes)

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: Erlang
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Concurrency / Networking
  • Software or Tool: Elixir, :gen_tcp
  • Main Book: “Elixir in Action” by Saša Jurić

What you’ll build: A TCP server that accepts multiple simultaneous connections, echoing back whatever clients send. Each connection is handled by its own lightweight BEAM process.

Why it teaches Phoenix: Before Phoenix, there’s Elixir. Before Elixir, there’s the BEAM. This project shows you the foundation—how lightweight processes work, how they communicate via messages, and why this model enables Phoenix’s performance.

Core challenges you’ll face:

  • Spawning processes for each connection → maps to the actor model
  • Message passing between processes → maps to how Phoenix Channels work
  • Handling process crashes → maps to fault tolerance
  • Using :gen_tcp → maps to understanding Cowboy’s foundation

Key Concepts:

  • BEAM Processes: “Elixir in Action” Chapter 5 - Saša Jurić
  • Message Passing: “Elixir in Action” Chapter 5
  • :gen_tcp module: Erlang/Elixir documentation
  • Process Linking: “Elixir in Action” Chapter 8

Difficulty: Intermediate Time estimate: Weekend Prerequisites: Basic Elixir syntax (pattern matching, functions, modules). Install Elixir.

Real world outcome:

# Terminal 1: Start your server
$ iex -S mix
iex> EchoServer.start(4000)
Listening on port 4000...

# Terminal 2: Connect with telnet
$ telnet localhost 4000
Connected to localhost.
Hello, BEAM!
Hello, BEAM!    # Server echoes back

# Terminal 3: Another simultaneous connection
$ telnet localhost 4000
Connected to localhost.
Second connection!
Second connection!

# Both connections work independently, each in its own process!

Implementation Hints:

The core pattern for accepting connections:

1. Open a listening socket with :gen_tcp.listen/2
2. Accept a connection with :gen_tcp.accept/1
3. Spawn a new process to handle this connection
4. Go back to step 2 (accept loop)

For each client process:
1. Receive data with :gen_tcp.recv/2
2. Send it back with :gen_tcp.send/2
3. Loop until connection closes

Key questions to answer:

  • What happens when you spawn a function?
  • How does receive block a process without blocking others?
  • What happens if a client process crashes? Does the server crash?
  • How many connections can you handle simultaneously?

Resources for key challenges:

Learning milestones:

  1. Single connection works → You understand :gen_tcp basics
  2. Multiple connections work simultaneously → You understand process spawning
  3. Server survives client crashes → You understand process isolation
  4. You can track active connections → You understand process communication

Project 2: Add Supervision Trees (Fault Tolerance)

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: Erlang
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: OTP / Fault Tolerance
  • Software or Tool: Elixir, OTP Supervisor
  • Main Book: “Elixir in Action” by Saša Jurić

What you’ll build: Wrap your TCP server in a supervision tree that automatically restarts failed components. Crash the acceptor? It restarts. Crash a connection handler? Only that connection dies.

Why it teaches Phoenix: Phoenix applications are OTP applications with supervision trees. Understanding Supervisors is essential for understanding how Phoenix stays resilient under load.

Core challenges you’ll face:

  • Designing a supervision tree → maps to Phoenix’s application structure
  • Choosing restart strategies → maps to one_for_one vs one_for_all
  • GenServer behavior → maps to how Channels and PubSub work
  • Application behavior → maps to how Phoenix apps start

Key Concepts:

  • Supervisors: “Elixir in Action” Chapter 8 - Saša Jurić
  • GenServer: “Elixir in Action” Chapter 6
  • Application Behavior: “Elixir in Action” Chapter 9
  • Restart Strategies: OTP documentation

Difficulty: Intermediate Time estimate: Weekend to 1 week Prerequisites: Project 1 (TCP server). Understanding of basic OTP concepts.

Real world outcome:

$ iex -S mix
iex> EchoServer.Application.start(:normal, [])
{:ok, #PID<0.150.0>}

# View the supervision tree
iex> :observer.start()
# Opens GUI showing:
#   EchoServer.Application
#   └── EchoServer.Supervisor
#       ├── EchoServer.Acceptor (GenServer)
#       └── EchoServer.ConnectionSupervisor (DynamicSupervisor)
#           ├── Connection #PID<0.200.0>
#           ├── Connection #PID<0.201.0>
#           └── Connection #PID<0.202.0>

# Kill the acceptor - it restarts automatically!
iex> Process.exit(pid, :kill)
# Logs: Acceptor crashed, restarting...
# Server continues working!

Implementation Hints:

Supervision tree structure:

Application
    │
    └── Supervisor (one_for_one)
            │
            ├── Acceptor (GenServer)
            │     └── Accepts connections, spawns handlers
            │
            └── ConnectionSupervisor (DynamicSupervisor)
                  └── Dynamically supervises connection handlers

GenServer callback skeleton:

defmodule EchoServer.Acceptor do
  use GenServer

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

  @impl true
  def init(port) do
    # Open listening socket
    {:ok, listen_socket} = :gen_tcp.listen(port, [...])
    # Start accepting (use send_after to avoid blocking init)
    send(self(), :accept)
    {:ok, %{socket: listen_socket}}
  end

  @impl true
  def handle_info(:accept, state) do
    # Accept connection, spawn handler, loop
    {:noreply, state}
  end
end

Learning milestones:

  1. Supervisor starts child processes → You understand supervision basics
  2. Crashed process restarts automatically → You understand restart strategies
  3. Dynamic supervisor manages connections → You understand DynamicSupervisor
  4. :observer shows your tree → You can visualize OTP applications

Project 3: Build a Minimal Plug Application

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 1. The “Resume Gold”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Web / HTTP
  • Software or Tool: Plug, Cowboy
  • Main Book: “Programming Phoenix 1.4” by Chris McCord

What you’ll build: A web application using only Plug and Cowboy—no Phoenix. You’ll see exactly what Phoenix does for you by doing it yourself: routing, parsing, rendering, and middleware.

Why it teaches Phoenix: Phoenix is built on Plug. Every Phoenix endpoint, router, and controller is a Plug. Understanding Plug means understanding Phoenix’s core abstraction.

Core challenges you’ll face:

  • The Plug specification → maps to function and module plugs
  • The Conn struct → maps to request/response data structure
  • Plug pipelines → maps to Phoenix pipelines
  • Plug.Router → maps to Phoenix.Router

Key Concepts:

  • Plug Specification: Plug Documentation
  • Conn Struct: Understanding request/response state
  • Cowboy: Erlang HTTP server
  • Pipelines: Composing transformations

Difficulty: Intermediate Time estimate: Weekend Prerequisites: Basic Elixir, understanding of HTTP. Projects 1-2 helpful but not required.

Real world outcome:

$ iex -S mix
iex> MiniWeb.Application.start(:normal, [])
Server running at http://localhost:4000

$ curl http://localhost:4000/
Welcome to MiniWeb!

$ curl http://localhost:4000/hello/world
Hello, world!

$ curl http://localhost:4000/users -d '{"name": "Alice"}'
Created user: Alice

$ curl http://localhost:4000/unknown
404 Not Found

Implementation Hints:

Minimal Plug module:

defmodule MiniWeb.Router do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/" do
    send_resp(conn, 200, "Welcome to MiniWeb!")
  end

  get "/hello/:name" do
    send_resp(conn, 200, "Hello, #{name}!")
  end

  match _ do
    send_resp(conn, 404, "Not Found")
  end
end

The Conn struct (simplified):

%Plug.Conn{
  host: "localhost",
  port: 4000,
  method: "GET",
  path_info: ["hello", "world"],
  params: %{"name" => "world"},
  req_headers: [...],
  resp_headers: [...],
  status: nil,        # Set by send_resp
  resp_body: nil,     # Set by send_resp
  assigns: %{},       # Your custom data
  ...
}

Key insight: A Plug is a function that takes a conn, transforms it, and returns a conn. That’s it!

Resources for key challenges:

Learning milestones:

  1. Cowboy serves your Plug → You understand the HTTP server layer
  2. Routes match correctly → You understand Plug.Router
  3. Custom plugs transform requests → You understand middleware
  4. You parse JSON bodies → You understand plug pipelines

Project 4: Your First Phoenix Application

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Web Framework
  • Software or Tool: Phoenix Framework
  • Main Book: “Programming Phoenix 1.4” by Chris McCord

What you’ll build: A standard Phoenix CRUD application—but with deep understanding of every generated file and concept. You’ll trace a request through the entire stack.

Why it teaches Phoenix: Now that you understand BEAM processes, OTP, and Plug, you can appreciate what Phoenix provides. This project connects the dots between the foundations and the framework.

Core challenges you’ll face:

  • Phoenix project structure → maps to where things live and why
  • Contexts (Phoenix 1.3+) → maps to domain-driven design
  • Ecto basics → maps to database interactions
  • Templates (HEEx) → maps to HTML generation

Key Concepts:

  • Phoenix Architecture: Phoenix Overview
  • Contexts: “Programming Phoenix 1.4” Chapter 2
  • Router & Pipelines: “Programming Phoenix 1.4” Chapter 2
  • Controllers & Views: “Programming Phoenix 1.4” Chapter 3

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 1-3 (foundations), basic SQL knowledge.

Real world outcome:

$ mix phx.new blog
$ cd blog
$ mix ecto.create
$ mix phx.gen.html Content Post posts title:string body:text
$ mix ecto.migrate
$ mix phx.server

# Browser: http://localhost:4000/posts
# Full CRUD interface for blog posts!

# You understand:
# - Why files are organized this way
# - How the router dispatches to controllers
# - How Ecto persists data
# - How templates render HTML
# - How the whole request flows through the stack

Implementation Hints:

Phoenix project structure:

blog/
├── lib/
│   ├── blog/              # Business logic (contexts)
│   │   ├── content.ex     # Content context
│   │   └── content/
│   │       └── post.ex    # Post schema
│   ├── blog_web/          # Web interface
│   │   ├── controllers/
│   │   ├── components/    # (Phoenix 1.7+) or templates/
│   │   ├── router.ex
│   │   └── endpoint.ex
│   ├── blog.ex            # Application module
│   └── blog_web.ex        # Web module macros
├── config/                # Configuration
├── priv/
│   └── repo/
│       └── migrations/    # Database migrations
└── test/                  # Tests

The request flow for GET /posts:

1. Endpoint receives HTTP request
2. Router matches "/posts" to PostController.index
3. Pipeline `:browser` applies session, flash, CSRF plugs
4. PostController.index calls Content.list_posts()
5. Context queries database via Ecto
6. Controller renders "index.html" with posts
7. Template generates HTML
8. Response sent to browser

Learning milestones:

  1. Generated app runs → You understand project structure
  2. You can trace a request through the stack → You understand the flow
  3. You modify a context function → You understand the boundary
  4. You add a new route and controller → You understand the patterns

Project 5: Deep Dive into Ecto

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Database / Functional
  • Software or Tool: Ecto, PostgreSQL
  • Main Book: “Programming Ecto” by Darin Wilson

What you’ll build: A data-intensive application that uses Ecto’s advanced features: complex queries, associations, transactions, custom types, and understanding why Ecto is NOT an ORM.

Why it teaches Phoenix: Ecto is Elixir’s database toolkit. Unlike ActiveRecord or Django ORM, it’s explicitly functional—no hidden state, explicit changesets, composable queries. Understanding Ecto’s philosophy is essential for Phoenix development.

Core challenges you’ll face:

  • Changesets → maps to validating and casting data
  • Composable queries → maps to building queries piece by piece
  • Associations → maps to has_many, belongs_to, many_to_many
  • Transactions → maps to multi-step database operations

Key Concepts:

  • Repos: “Programming Ecto” Chapter 2 - All DB operations go through Repo
  • Schemas: “Programming Ecto” Chapter 3 - Mapping DB to Elixir
  • Changesets: “Programming Ecto” Chapter 4 - Validating changes
  • Query: “Programming Ecto” Chapter 5 - Composable queries

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 4 (basic Phoenix), SQL knowledge.

Real world outcome:

# Complex composable query
query = from p in Post,
  join: u in assoc(p, :user),
  where: p.published == true,
  where: p.inserted_at > ^one_week_ago,
  order_by: [desc: p.inserted_at],
  preload: [:user, :comments],
  select: %{title: p.title, author: u.name, comment_count: count(p.comments)}

posts = Repo.all(query)

# Changeset with validations
def changeset(user, attrs) do
  user
  |> cast(attrs, [:email, :password, :name])
  |> validate_required([:email, :password])
  |> validate_format(:email, ~r/@/)
  |> validate_length(:password, min: 8)
  |> unique_constraint(:email)
  |> put_password_hash()
end

# Transaction for complex operations
Repo.transaction(fn ->
  with {:ok, user} <- Accounts.create_user(attrs),
       {:ok, profile} <- Profiles.create_profile(user, profile_attrs),
       :ok <- Mailer.send_welcome_email(user) do
    {:ok, user}
  else
    {:error, reason} -> Repo.rollback(reason)
  end
end)

Implementation Hints:

Ecto is NOT an ORM - key differences:

ActiveRecord (ORM)                    Ecto (Functional Toolkit)
──────────────────                    ──────────────────────────

user.save                             Repo.insert(changeset)
  ↓                                     ↓
Object tracks its own                 Changeset is passed
dirty state                           explicitly to Repo

user.posts.build(...)                 build_assoc(user, :posts, ...)
  ↓                                     ↓
Implicit association                  Explicit function call
magic

User.find(1)                          Repo.get(User, 1)
  ↓                                     ↓
Model has class methods               Repo handles all queries

user.posts                            Repo.preload(user, :posts)
  ↓                                     ↓
Lazy loading (N+1 trap)               Explicit preloading

Resources for key challenges:

Learning milestones:

  1. You write composable queries → You understand Ecto.Query
  2. Changeset validates and transforms → You understand the pattern
  3. Associations load explicitly → You understand preloading
  4. Transaction handles failures → You understand Multi and rollbacks

Project 6: Phoenix Channels (Real-Time Communication)

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir + JavaScript
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 4: Hardcore Tech Flex
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Real-Time / WebSockets
  • Software or Tool: Phoenix Channels, JavaScript client
  • Main Book: “Programming Phoenix 1.4” by Chris McCord

What you’ll build: A real-time chat application where messages appear instantly for all users without page refresh. You’ll understand Phoenix’s pub-sub system and how millions of connections are handled.

Why it teaches Phoenix: Channels showcase Phoenix’s killer feature: real-time at scale. Each WebSocket connection is a lightweight BEAM process. Phoenix PubSub distributes messages. This is why Discord uses Elixir.

Core challenges you’ll face:

  • Socket lifecycle → maps to connect, join, handle_in, terminate
  • Topics and rooms → maps to pub-sub patterns
  • Presence → maps to tracking who’s online
  • JavaScript client → maps to browser-side integration

Key Concepts:

  • Channels: “Programming Phoenix 1.4” Chapter 11
  • PubSub: Phoenix.PubSub documentation
  • Presence: Phoenix.Presence documentation
  • JavaScript Client: phoenix.js documentation

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Project 4 (Phoenix basics), JavaScript knowledge.

Real world outcome:

┌─────────────────────────────────────────────────────────────────┐
│  Phoenix Chat - Room: #general                                  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Alice: Hey everyone!                              10:30 AM     │
│  Bob: Hi Alice! How's it going?                    10:31 AM     │
│  Charlie: Great to see you both!                   10:31 AM     │
│                                                                 │
│  ─────────────────────────────────────────────────────────────  │
│  Online: Alice, Bob, Charlie (3 users)                          │
│  ─────────────────────────────────────────────────────────────  │
│                                                                 │
│  [Type a message...]                                    [Send]  │
└─────────────────────────────────────────────────────────────────┘

# Messages appear INSTANTLY for all connected users
# User list updates in real-time as people join/leave
# Each user is a separate BEAM process on the server

Implementation Hints:

Channel lifecycle:

Browser                                         Phoenix
   │                                               │
   │── WebSocket connect ─────────────────────────►│
   │                                    UserSocket.connect/3
   │◄───────────────────── :ok ────────────────────│
   │                                               │
   │── channel.join("room:lobby") ────────────────►│
   │                                    RoomChannel.join/3
   │◄───────────────────── :ok ────────────────────│
   │                                               │
   │── channel.push("new_msg", {body: "Hi"}) ─────►│
   │                                    RoomChannel.handle_in/3
   │                                    broadcast!(socket, "new_msg", ...)
   │◄─────────── broadcast to all in room ─────────│
   │                                               │

Channel module structure:

defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel

  def join("room:" <> room_id, _params, socket) do
    # Called when client joins this topic
    {:ok, assign(socket, :room_id, room_id)}
  end

  def handle_in("new_msg", %{"body" => body}, socket) do
    # Handle incoming message from this client
    broadcast!(socket, "new_msg", %{body: body, user: socket.assigns.user})
    {:noreply, socket}
  end
end

Resources for key challenges:

Learning milestones:

  1. WebSocket connects → You understand Socket
  2. Messages broadcast to all → You understand pub-sub
  3. Presence tracks online users → You understand Presence
  4. Multiple rooms work → You understand topics

Project 7: Phoenix LiveView (Interactive UI without JavaScript)

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir (with minimal JavaScript)
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Real-Time / UI
  • Software or Tool: Phoenix LiveView
  • Main Book: “Programming Phoenix LiveView” by Bruce Tate

What you’ll build: A fully interactive single-page application with search-as-you-type, form validation, sorting, pagination, and real-time updates—all without writing JavaScript. LiveView sends minimal DOM diffs over WebSocket.

Why it teaches Phoenix: LiveView is Phoenix’s most innovative feature. It combines the productivity of server-rendered apps with the interactivity of SPAs. Understanding how it tracks state and computes diffs teaches you the framework’s core philosophy.

Core challenges you’ll face:

  • LiveView lifecycle → maps to mount, handle_event, render
  • Socket assigns → maps to state management
  • DOM patching → maps to how minimal updates work
  • Live navigation → maps to SPA-like routing

Key Concepts:

Difficulty: Advanced Time estimate: 2-3 weeks Prerequisites: Projects 4-6 (Phoenix and Channels). Understanding of HTML/CSS.

Real world outcome:

┌─────────────────────────────────────────────────────────────────┐
│  Product Catalog                            [🔍 Search...]      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Sort by: [Name ▼] [Price ▼]           Filter: [All Categories] │
│                                                                 │
│  ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐       │
│  │  📱       │ │  💻       │ │  🎧       │ │  ⌚       │       │
│  │  iPhone   │ │  MacBook  │ │  AirPods  │ │  Watch    │       │
│  │  $999     │ │  $1,299   │ │  $249     │ │  $399     │       │
│  │ [Add 🛒]  │ │ [Add 🛒]  │ │ [Add 🛒]  │ │ [Add 🛒]  │       │
│  └───────────┘ └───────────┘ └───────────┘ └───────────┘       │
│                                                                 │
│  Showing 1-4 of 24 products    [◀ Prev] [1] [2] [3] [Next ▶]   │
│                                                                 │
│  Cart: 3 items ($1,647)                            [Checkout]   │
└─────────────────────────────────────────────────────────────────┘

# As you type in search: results filter instantly
# Click column header: sorts without page reload
# Add to cart: cart updates, counter changes
# ALL without writing JavaScript—just Elixir!

Implementation Hints:

LiveView module structure:

defmodule MyAppWeb.ProductLive.Index do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    # Called on initial load AND WebSocket connect
    products = Products.list_products()
    {:ok, assign(socket, products: products, cart: [])}
  end

  def handle_event("search", %{"query" => query}, socket) do
    # Called when user types in search box
    products = Products.search(query)
    {:noreply, assign(socket, products: products)}
  end

  def handle_event("add_to_cart", %{"id" => id}, socket) do
    product = Products.get_product!(id)
    cart = [product | socket.assigns.cart]
    {:noreply, assign(socket, cart: cart)}
  end

  def render(assigns) do
    ~H"""
    <input type="text" phx-keyup="search" placeholder="Search..." />
    <div class="products">
      <%= for product <- @products do %>
        <div class="product">
          <h3><%= product.name %></h3>
          <button phx-click="add_to_cart" phx-value-id={product.id}>
            Add to Cart
          </button>
        </div>
      <% end %>
    </div>
    <div class="cart">Items: <%= length(@cart) %></div>
    """
  end
end

How diff tracking works:

Template divides into static and dynamic parts:

~H"""
<div class="product">           ← static (sent once)
  <h3><%= @product.name %></h3> ← dynamic (tracked)
  <p>$<%= @product.price %></p> ← dynamic (tracked)
</div>                          ← static (sent once)
"""

When @product.price changes from 99 to 89:
- Server ONLY sends: {position_2: "89"}
- Client patches just that text node
- No full HTML re-render!

Resources for key challenges:

Learning milestones:

  1. Mount and render work → You understand the lifecycle
  2. Events update state → You understand handle_event
  3. You see minimal diff in DevTools → You understand optimization
  4. Live navigation works → You understand SPA-like behavior

Project 8: Authentication from Scratch

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Security / Web
  • Software or Tool: Phoenix, Argon2, JWT (optional)
  • Main Book: “Programming Phoenix 1.4” by Chris McCord

What you’ll build: A complete authentication system with registration, login, logout, password hashing, session management, and protected routes. You’ll understand both how phx.gen.auth works and build the key pieces manually.

Why it teaches Phoenix: Authentication touches many Phoenix concepts: plugs (for auth checks), contexts (for user management), Ecto (for password hashing), sessions, and LiveView integration. It’s a great integration project.

Core challenges you’ll face:

  • Password hashing → maps to bcrypt/argon2, never store plaintext
  • Session management → maps to cookies, tokens, security
  • Auth plugs → maps to protecting routes
  • LiveView auth → maps to socket assigns, on_mount

Key Concepts:

  • Password Hashing: Comeonin/Argon2 libraries
  • Plug.Session: Cookie-based sessions
  • CSRF Protection: Phoenix built-in
  • phx.gen.auth: Phoenix auth generator

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 4-5 (Phoenix, Ecto), understanding of web security basics.

Real world outcome:

# Registration
POST /users/register
{email: "alice@example.com", password: "secret123"}
 Creates user with hashed password
 Sets session token
 Redirects to dashboard

# Login
POST /users/login
{email: "alice@example.com", password: "secret123"}
 Verifies password hash
 Sets session token
 Redirects to dashboard

# Protected route
GET /dashboard
 Auth plug checks session
 If valid: shows dashboard with current_user
 If invalid: redirects to login

# LiveView protected page
 on_mount hook assigns current_user to socket
 If no user: redirect to login

Implementation Hints:

Auth plug for protecting routes:

defmodule MyAppWeb.Plugs.RequireAuth do
  import Plug.Conn
  import Phoenix.Controller

  def init(opts), do: opts

  def call(conn, _opts) do
    if conn.assigns[:current_user] do
      conn
    else
      conn
      |> put_flash(:error, "You must log in to access this page")
      |> redirect(to: "/login")
      |> halt()
    end
  end
end

# In router:
pipeline :protected do
  plug :fetch_current_user
  plug MyAppWeb.Plugs.RequireAuth
end

Password hashing with Argon2:

# In changeset
def registration_changeset(user, attrs) do
  user
  |> cast(attrs, [:email, :password])
  |> validate_required([:email, :password])
  |> validate_length(:password, min: 8)
  |> hash_password()
end

defp hash_password(changeset) do
  case get_change(changeset, :password) do
    nil -> changeset
    password ->
      put_change(changeset, :password_hash, Argon2.hash_pwd_salt(password))
  end
end

Learning milestones:

  1. Passwords are hashed correctly → You understand security basics
  2. Sessions persist across requests → You understand session management
  3. Protected routes redirect → You understand auth plugs
  4. LiveView pages are protected → You understand on_mount hooks

Project 9: Build a REST API with JSON:API or GraphQL

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: API Design
  • Software or Tool: Phoenix, Absinthe (for GraphQL), or JSON:API
  • Main Book: “Craft GraphQL APIs in Elixir with Absinthe” by Bruce Williams

What you’ll build: A well-designed API (REST or GraphQL) with authentication, pagination, filtering, proper error handling, and documentation. You’ll understand Phoenix’s API capabilities.

Why it teaches Phoenix: Phoenix isn’t just for HTML. Its lightweight nature and Elixir’s pattern matching make it excellent for APIs. Understanding the :api pipeline versus :browser teaches important architectural decisions.

Core challenges you’ll face:

  • API pipeline → maps to JSON rendering, no session
  • Error handling → maps to fallback controllers
  • GraphQL types → maps to Absinthe schema
  • Pagination → maps to cursor vs offset pagination

Key Concepts:

  • Phoenix JSON API: Phoenix documentation
  • Absinthe GraphQL: “Craft GraphQL APIs in Elixir with Absinthe”
  • API Authentication: Token-based auth
  • Error Handling: FallbackController pattern

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: Projects 4-5 (Phoenix, Ecto), understanding of REST/GraphQL concepts.

Real world outcome:

# REST API
$ curl -H "Authorization: Bearer <token>" \
       http://localhost:4000/api/posts
{
  "data": [
    {"id": 1, "title": "Hello Phoenix", "author": "Alice"},
    {"id": 2, "title": "Elixir Rocks", "author": "Bob"}
  ],
  "meta": {"total": 42, "page": 1, "per_page": 10}
}

# GraphQL API
$ curl -X POST http://localhost:4000/graphql \
       -H "Content-Type: application/json" \
       -d '{"query": "{ posts { title author { name } } }"}'
{
  "data": {
    "posts": [
      {"title": "Hello Phoenix", "author": {"name": "Alice"}},
      {"title": "Elixir Rocks", "author": {"name": "Bob"}}
    ]
  }
}

Implementation Hints:

API pipeline (no session, HTML rendering):

pipeline :api do
  plug :accepts, ["json"]
  plug MyAppWeb.Plugs.APIAuth
end

scope "/api", MyAppWeb.API do
  pipe_through :api

  resources "/posts", PostController, except: [:new, :edit]
end

FallbackController for error handling:

defmodule MyAppWeb.FallbackController do
  use MyAppWeb, :controller

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> json(%{error: "Not found"})
  end

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> json(%{errors: format_errors(changeset)})
  end
end

Learning milestones:

  1. API returns JSON → You understand the api pipeline
  2. Auth protects endpoints → You understand token auth
  3. Errors return proper status codes → You understand FallbackController
  4. GraphQL queries work → You understand Absinthe

Project 10: PubSub and Distributed Phoenix

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 4: Expert
  • Knowledge Area: Distributed Systems
  • Software or Tool: Phoenix.PubSub, libcluster
  • Main Book: “Elixir in Action” by Saša Jurić

What you’ll build: A Phoenix application that runs on multiple nodes, with PubSub messages reaching all connected users across all nodes. You’ll see how Phoenix scales horizontally.

Why it teaches Phoenix: Phoenix’s ability to scale across multiple nodes—with Channels, LiveView, and PubSub working seamlessly—is why companies choose it for high-traffic applications. Understanding distribution is understanding Phoenix’s power.

Core challenges you’ll face:

  • Connecting BEAM nodes → maps to libcluster, Erlang distribution
  • PubSub across nodes → maps to :pg2, Phoenix.PubSub
  • Session sharing → maps to Redis, distributed cache
  • Deployment → maps to releases, clustering in production

Key Concepts:

  • Erlang Distribution: Node connections, cookies
  • PubSub Adapters: PG2 (built-in), Redis
  • libcluster: Auto-discovery of nodes
  • Releases: mix release

Difficulty: Expert Time estimate: 2-3 weeks Prerequisites: Projects 6-7 (Channels, LiveView), understanding of distributed systems.

Real world outcome:

# Start node 1
$ PORT=4000 iex --name node1@127.0.0.1 -S mix phx.server

# Start node 2
$ PORT=4001 iex --name node2@127.0.0.1 -S mix phx.server

# Nodes discover each other (libcluster)
[libcluster] Connected to node2@127.0.0.1

# User A connects to node1, User B connects to node2
# User A sends message in chat
# User B receives it INSTANTLY (PubSub across nodes)

# In iex on node1:
iex(node1@127.0.0.1)> Node.list()
[:node2@127.0.0.1]

iex(node1@127.0.0.1)> Phoenix.PubSub.broadcast(MyApp.PubSub, "room:lobby", {:msg, "Hi!"})
# Message received on BOTH nodes!

Implementation Hints:

libcluster configuration:

# config/runtime.exs
config :libcluster,
  topologies: [
    local: [
      strategy: Cluster.Strategy.Gossip
    ]
  ]

PubSub across nodes:

Node 1                                 Node 2
┌─────────────────────┐              ┌─────────────────────┐
│ Phoenix.PubSub      │              │ Phoenix.PubSub      │
│ (local subscribers) │◄────────────►│ (local subscribers) │
│                     │   pg2/Redis  │                     │
│ User A (WebSocket)  │              │ User B (WebSocket)  │
└─────────────────────┘              └─────────────────────┘

When User A sends message:
1. Channel broadcasts to local PubSub
2. PubSub adapter forwards to other nodes
3. Other nodes broadcast to their local subscribers
4. User B receives message

Learning milestones:

  1. Two nodes connect → You understand Erlang distribution
  2. PubSub reaches both nodes → You understand distributed PubSub
  3. LiveView works across nodes → You understand horizontal scaling
  4. You can add/remove nodes dynamically → You understand clustering

Project 11: Background Jobs with Oban

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Background Processing
  • Software or Tool: Oban
  • Main Book: Oban documentation

What you’ll build: A robust background job system for sending emails, processing images, syncing data, and other tasks that shouldn’t block web requests. You’ll understand job queues, retries, and scheduling.

Why it teaches Phoenix: Real applications need background processing. Oban is the standard for Elixir, leveraging PostgreSQL for reliability. Understanding async patterns complements your synchronous Phoenix knowledge.

Core challenges you’ll face:

  • Job definition → maps to workers and args
  • Queues and concurrency → maps to controlling parallelism
  • Retries and backoff → maps to handling failures
  • Scheduling → maps to cron-like recurring jobs

Key Concepts:

  • Oban Workers: Defining job behavior
  • Queues: Controlling concurrency
  • Pruning: Cleaning old jobs
  • Telemetry: Monitoring job execution

Difficulty: Advanced Time estimate: 1 week Prerequisites: Project 4-5 (Phoenix, Ecto), PostgreSQL.

Real world outcome:

# Enqueue a job
%{user_id: 123, email: "welcome"}
|> MyApp.Workers.SendEmail.new()
|> Oban.insert()

# Job executes in background
# defmodule MyApp.Workers.SendEmail do
#   use Oban.Worker, queue: :emails, max_attempts: 3
#
#   def perform(%Job{args: %{"user_id" => user_id, "email" => email}}) do
#     user = Accounts.get_user!(user_id)
#     Mailer.send(user, email)
#   end
# end

# Web UI shows:
# ┌────────────────────────────────────────────────────────────┐
# │ Oban Dashboard                                             │
# ├────────────────────────────────────────────────────────────┤
# │ Queues:                                                    │
# │   emails:     5 available, 2 executing, 0 failed           │
# │   images:    10 available, 5 executing, 1 retrying         │
# │   sync:       0 available, 0 executing, 0 failed           │
# │                                                            │
# │ Recent Jobs:                                               │
# │   SendEmail (user:123) - completed 5s ago                  │
# │   ProcessImage (file:abc) - executing...                   │
# │   SendEmail (user:124) - completed 10s ago                 │
# └────────────────────────────────────────────────────────────┘

Implementation Hints:

Oban worker with retry:

defmodule MyApp.Workers.ProcessImage do
  use Oban.Worker,
    queue: :images,
    max_attempts: 5,
    priority: 1

  @impl Oban.Worker
  def perform(%Job{args: %{"image_id" => id}}) do
    image = Media.get_image!(id)

    case ImageProcessor.resize(image) do
      {:ok, resized} ->
        Media.update_image(image, %{processed: true, url: resized.url})
        :ok

      {:error, reason} ->
        # Returning error triggers retry with backoff
        {:error, reason}
    end
  end
end

Learning milestones:

  1. Jobs execute in background → You understand async processing
  2. Failed jobs retry → You understand error handling
  3. Queues control concurrency → You understand resource management
  4. Scheduled jobs run on time → You understand cron-like behavior

Project 12: Telemetry and Observability

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: Monitoring / Observability
  • Software or Tool: Telemetry, PromEx, LiveDashboard
  • Main Book: “Elixir in Action” by Saša Jurić

What you’ll build: Full observability for your Phoenix app: request timing, database query metrics, custom business metrics, Prometheus integration, and live dashboards.

Why it teaches Phoenix: Phoenix is built on Telemetry—it emits events for everything. Understanding how to capture and act on these events is essential for running Phoenix in production.

Core challenges you’ll face:

  • Telemetry events → maps to what Phoenix emits
  • Handlers and metrics → maps to collecting data
  • LiveDashboard → maps to built-in monitoring
  • Prometheus/Grafana → maps to production monitoring

Key Concepts:

  • :telemetry library: Event emission and handling
  • Phoenix.Telemetry: Built-in Phoenix events
  • PromEx: Prometheus metrics for Phoenix
  • LiveDashboard: Built-in Phoenix dashboard

Difficulty: Advanced Time estimate: 1 week Prerequisites: Project 4 (Phoenix basics), understanding of metrics concepts.

Real world outcome:

LiveDashboard (http://localhost:4000/dashboard)
┌─────────────────────────────────────────────────────────────────┐
│ Phoenix LiveDashboard                                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ Home │ Metrics │ Request Logger │ Applications │ Processes     │
│                                                                 │
│ System:                                                         │
│   BEAM Memory: 234 MB │ Atoms: 12,453 │ Processes: 1,234       │
│   CPU: 15% │ Run Queue: 0                                       │
│                                                                 │
│ Phoenix:                                                        │
│   Requests/sec: 1,234 │ Avg Latency: 12ms │ 99p: 45ms          │
│   WebSocket connections: 567                                    │
│                                                                 │
│ Ecto:                                                           │
│   Queries/sec: 2,345 │ Avg Time: 2ms │ Pool Size: 10           │
│                                                                 │
│ Custom Metrics:                                                 │
│   Signups today: 123 │ Orders: 45 │ Revenue: $12,345           │
└─────────────────────────────────────────────────────────────────┘

Implementation Hints:

Attaching to Phoenix telemetry events:

# In application.ex
def start(_type, _args) do
  :telemetry.attach_many(
    "my-app-handler",
    [
      [:phoenix, :endpoint, :stop],
      [:my_app, :repo, :query],
      [:my_app, :user, :signup]
    ],
    &MyApp.Telemetry.handle_event/4,
    nil
  )
  # ... start supervision tree
end

# Handler
defmodule MyApp.Telemetry do
  def handle_event([:phoenix, :endpoint, :stop], measurements, metadata, _config) do
    Logger.info("Request to #{metadata.route} took #{measurements.duration / 1_000_000}ms")
  end
end

Learning milestones:

  1. LiveDashboard shows metrics → You understand built-in observability
  2. Custom events emit → You understand Telemetry API
  3. Prometheus scrapes metrics → You understand external monitoring
  4. Grafana dashboard works → You have production observability

Project 13: Testing Phoenix Applications

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 2: Practical but Forgettable
  • Business Potential: 2. The “Micro-SaaS / Pro Tool”
  • Difficulty: Level 2: Intermediate
  • Knowledge Area: Testing / Quality
  • Software or Tool: ExUnit, Mox, Wallaby
  • Main Book: “Testing Elixir” by Andrea Leopardi

What you’ll build: A comprehensive test suite covering unit tests, integration tests, controller tests, LiveView tests, and end-to-end browser tests. You’ll understand Phoenix’s testing patterns.

Why it teaches Phoenix: Phoenix has excellent testing support built-in. Understanding how to test each layer—contexts, controllers, channels, LiveView—makes you a more effective Phoenix developer.

Core challenges you’ll face:

  • ConnTest → maps to testing controllers
  • DataCase → maps to testing with database
  • ChannelCase → maps to testing channels
  • LiveViewTest → maps to testing LiveView

Key Concepts:

  • ExUnit: Elixir’s testing framework
  • ConnTest: Phoenix controller testing
  • Mox: Behavior-based mocking
  • Sandbox: Database isolation

Difficulty: Intermediate Time estimate: 1 week Prerequisites: Projects 4-7 (Phoenix, Ecto, Channels, LiveView).

Real world outcome:

$ mix test
...............................................................

Finished in 2.3 seconds
42 tests, 0 failures

Randomized with seed 12345

# Test breakdown:
# - 15 context tests (pure business logic)
# - 10 controller tests (HTTP layer)
# - 8 LiveView tests (interactive UI)
# - 5 channel tests (real-time)
# - 4 integration tests (full stack)

Implementation Hints:

Testing a controller:

defmodule MyAppWeb.PostControllerTest do
  use MyAppWeb.ConnCase

  describe "index" do
    test "lists all posts", %{conn: conn} do
      post = insert(:post, title: "Hello")

      conn = get(conn, ~p"/posts")

      assert html_response(conn, 200) =~ "Hello"
    end
  end
end

Testing LiveView:

defmodule MyAppWeb.CounterLiveTest do
  use MyAppWeb.ConnCase
  import Phoenix.LiveViewTest

  test "increments counter", %{conn: conn} do
    {:ok, view, _html} = live(conn, "/counter")

    assert view |> element("span.count") |> render() =~ "0"

    view |> element("button", "Increment") |> render_click()

    assert view |> element("span.count") |> render() =~ "1"
  end
end

Learning milestones:

  1. Context tests pass → You test business logic
  2. Controller tests pass → You test HTTP layer
  3. LiveView tests pass → You test interactive UIs
  4. Full test suite is fast → You understand async testing

Project 14: Deployment and Production Phoenix

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: Docker, Bash
  • Coolness Level: Level 3: Genuinely Clever
  • Business Potential: 3. The “Service & Support” Model
  • Difficulty: Level 3: Advanced
  • Knowledge Area: DevOps / Deployment
  • Software or Tool: Mix releases, Docker, Fly.io or self-hosted
  • Main Book: “Real-World Elixir Deployment” (various resources)

What you’ll build: A production-ready Phoenix deployment with releases, environment configuration, migrations, health checks, and clustering across multiple instances.

Why it teaches Phoenix: Development is only half the story. Understanding how to package Phoenix as a release, handle secrets, run migrations, and scale horizontally completes your knowledge.

Core challenges you’ll face:

  • Releases → maps to packaging for production
  • Runtime configuration → maps to config/runtime.exs
  • Migrations in production → maps to Ecto.Migrator
  • Clustering → maps to connecting nodes

Key Concepts:

  • Mix Releases: mix release
  • Runtime Config: config/runtime.exs
  • Docker: Containerized deployment
  • Health Checks: Ready/alive probes

Difficulty: Advanced Time estimate: 1-2 weeks Prerequisites: All previous projects, Docker knowledge helpful.

Real world outcome:

# Dockerfile
FROM hexpm/elixir:1.15.0-erlang-26.0-alpine-3.18.0 AS build
# ... build release

FROM alpine:3.18.0
COPY --from=build /app/_build/prod/rel/my_app ./
CMD ["bin/my_app", "start"]
# Build and deploy
$ docker build -t my_app .
$ docker push my_registry/my_app

# On Fly.io
$ fly deploy

# Check clustering
$ fly ssh console
> Node.list()
[:my_app@fdaa:0:1234::3]  # Connected to peer!

# Run migrations
$ fly ssh console
> MyApp.Release.migrate()

Implementation Hints:

Release configuration:

# config/runtime.exs
import Config

if config_env() == :prod do
  config :my_app, MyApp.Repo,
    url: System.get_env("DATABASE_URL"),
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")

  config :my_app, MyAppWeb.Endpoint,
    url: [host: System.get_env("PHX_HOST")],
    secret_key_base: System.get_env("SECRET_KEY_BASE")
end

Release module for migrations:

defmodule MyApp.Release do
  def migrate do
    for repo <- repos() do
      {:ok, _, _} = Ecto.Migrator.with_repo(repo, &Ecto.Migrator.run(&1, :up, all: true))
    end
  end

  defp repos, do: Application.fetch_env!(:my_app, :ecto_repos)
end

Learning milestones:

  1. Release builds successfully → You understand mix release
  2. Docker container runs → You understand containerization
  3. App starts in production → You understand runtime config
  4. Multiple instances cluster → You understand horizontal scaling

Project Comparison Table

Project Difficulty Time Depth of Understanding Fun Factor
1. TCP Echo Server Intermediate Weekend ⭐⭐⭐⭐⭐ ⭐⭐⭐
2. Supervision Trees Intermediate Weekend-1wk ⭐⭐⭐⭐⭐ ⭐⭐⭐
3. Minimal Plug App Intermediate Weekend ⭐⭐⭐⭐ ⭐⭐⭐⭐
4. First Phoenix App Intermediate 1 week ⭐⭐⭐⭐ ⭐⭐⭐⭐
5. Ecto Deep Dive Advanced 1-2 weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐
6. Phoenix Channels Advanced 1-2 weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
7. Phoenix LiveView Advanced 2-3 weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
8. Authentication Advanced 1-2 weeks ⭐⭐⭐⭐ ⭐⭐⭐
9. REST/GraphQL API Advanced 1-2 weeks ⭐⭐⭐⭐ ⭐⭐⭐⭐
10. Distributed Phoenix Expert 2-3 weeks ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
11. Background Jobs Advanced 1 week ⭐⭐⭐⭐ ⭐⭐⭐
12. Telemetry Advanced 1 week ⭐⭐⭐⭐ ⭐⭐⭐
13. Testing Intermediate 1 week ⭐⭐⭐⭐ ⭐⭐
14. Deployment Advanced 1-2 weeks ⭐⭐⭐⭐ ⭐⭐⭐

If you’re new to Elixir and Phoenix:

  1. Learn Elixir basics (pattern matching, modules, recursion)
  2. Project 1: TCP Echo Server → Understand BEAM processes
  3. Project 2: Supervision Trees → Understand OTP
  4. Project 3: Minimal Plug → Understand the web layer
  5. Project 4: First Phoenix App → Connect the dots
  6. Projects 5-7 → Deep dive into Ecto, Channels, LiveView
  7. Continue with remaining projects

If you know another web framework (Rails, Django, Express):

  1. Quick Elixir syntax review
  2. Project 1-2 (Quick: understand the concurrency model)
  3. Project 4: First Phoenix App → See Phoenix conventions
  4. Project 7: LiveView → See what’s unique
  5. Project 10: Distributed Phoenix → See the scalability story
  6. Fill in gaps as needed

If you want to understand Phoenix internals:

  1. Projects 1-3 (essential: understand the layers)
  2. Read Phoenix source code (it’s very readable!)
  3. Project 6-7 (Channels, LiveView internals)
  4. Project 10 (Distribution)
  5. Consider “Build Your Own Web Framework in Elixir” book

Final Capstone Project: Real-Time Collaborative Application

  • File: LEARN_PHOENIX_FRAMEWORK_DEEP_DIVE.md
  • Main Programming Language: Elixir
  • Alternative Programming Languages: N/A
  • Coolness Level: Level 5: Pure Magic
  • Business Potential: 4. The “Open Core” Infrastructure
  • Difficulty: Level 5: Master
  • Knowledge Area: Full-Stack Phoenix
  • Software or Tool: All Phoenix features
  • Main Book: All referenced books

What you’ll build: A real-time collaborative document editor (like a mini Google Docs) where multiple users can edit the same document simultaneously, see each other’s cursors, and changes merge correctly.

This project integrates:

  • BEAM Processes: Each document is a GenServer
  • OTP: Supervision for document processes
  • Plug: Request pipeline
  • Phoenix: Web framework structure
  • Ecto: Document persistence
  • Channels: Real-time sync
  • LiveView: Interactive UI
  • PubSub: Multi-node support
  • Presence: Who’s editing what
  • Telemetry: Monitoring
  • Deployment: Production clustering

Why this is the ultimate Phoenix project: It demonstrates everything Phoenix excels at—real-time, stateful, distributed, fault-tolerant. Companies like Figma, Notion, and Google Docs face these challenges. Building even a simple version proves mastery.

Real world outcome:

┌─────────────────────────────────────────────────────────────────┐
│  Collaborative Editor - Document: "Team Notes"                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Online: 🟢 Alice (editing)  🟢 Bob (viewing)  🟢 Charlie       │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                                                         │   │
│  │  # Meeting Notes                                        │   │
│  │                                                         │   │
│  │  ## Decisions                                           │   │
│  │  - Use Phoenix for the backend ← Alice's cursor         │   │
│  │  - Deploy on Fly.io|                                    │   │
│  │                     ↑ Bob is typing here                │   │
│  │  ## Action Items                                        │   │
│  │  - [ ] Set up CI/CD                                     │   │
│  │                                                         │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  💾 Auto-saved 2 seconds ago                                    │
└─────────────────────────────────────────────────────────────────┘

Features:
- Real-time sync across all connected users
- Cursor positions shown for other users
- Conflict resolution for concurrent edits
- Works across multiple server nodes
- Survives server restarts (document state recovered)
- Presence shows who's online

Summary

# Project Main Language
1 TCP Echo Server (BEAM Processes) Elixir
2 Supervision Trees (Fault Tolerance) Elixir
3 Minimal Plug Application Elixir
4 First Phoenix Application Elixir
5 Deep Dive into Ecto Elixir
6 Phoenix Channels (Real-Time) Elixir + JavaScript
7 Phoenix LiveView (Interactive UI) Elixir
8 Authentication from Scratch Elixir
9 REST API with JSON:API or GraphQL Elixir
10 PubSub and Distributed Phoenix Elixir
11 Background Jobs with Oban Elixir
12 Telemetry and Observability Elixir
13 Testing Phoenix Applications Elixir
14 Deployment and Production Elixir + Docker
Final Real-Time Collaborative Editor (Capstone) Elixir

Key Resources Referenced

Books

  • “Elixir in Action” by Saša Jurić (2nd Edition)
  • “Programming Phoenix 1.4” by Chris McCord, Bruce Tate, José Valim
  • “Programming Phoenix LiveView” by Bruce Tate and Sophie DeBenedetto
  • “Programming Ecto” by Darin Wilson and Eric Meadows-Jönsson
  • “Craft GraphQL APIs in Elixir with Absinthe” by Bruce Williams
  • “Build Your Own Web Framework in Elixir” by Aditya Iyengar

Online Resources