Intermediate
Want to build production-ready Elixir applications? This tutorial covers OTP platform essentials, Phoenix web development, and production patterns needed for real-world Elixir systems.
Coverage
This tutorial covers 60-85% of Elixir knowledge - production-grade OTP and web development.
Prerequisites
- Beginner Tutorial complete
- Strong understanding of processes and message passing
- Familiarity with pattern matching and functional programming
- Comfortable with recursion and immutability
Learning Outcomes
By the end of this tutorial, you will:
- Build stateful services with GenServer
- Design supervision trees for fault tolerance
- Understand OTP application structure and configuration
- Build web applications with Phoenix framework
- Create real-time interfaces with LiveView
- Work with databases using Ecto
- Implement concurrent patterns with Task and Agent
- Write comprehensive tests for OTP applications
- Deploy Elixir applications to production
Learning Path
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
A[GenServer ⭐] --> B[Supervisor ⭐]
B --> C[Application]
C --> D[Task & Agent]
D --> E[Phoenix Framework ⭐]
E --> F[LiveView]
F --> G[Ecto]
G --> H[Testing Strategies]
H --> I[Configuration]
I --> J[Production Patterns]
style A fill:#DE8F05,stroke:#000000,stroke-width:3px,color:#000000
style B fill:#DE8F05,stroke:#000000,stroke-width:3px,color:#000000
style E fill:#DE8F05,stroke:#000000,stroke-width:3px,color:#000000
Color Palette: Orange (#DE8F05 - critical sections for production Elixir)
⭐ Most important sections: GenServer, Supervisor, and Phoenix - master these for production readiness!
Section 1: GenServer - Building Stateful Services
GenServer (Generic Server) is the foundation of stateful services in Elixir.
Understanding GenServer
GenServer provides:
- State management: Maintain process state across calls
- Synchronous calls: Request-response pattern with
call/3 - Asynchronous casts: Fire-and-forget with
cast/2 - Info messages: Handle arbitrary messages with
handle_info/2 - Lifecycle hooks: Initialize, terminate, code change
Basic GenServer Implementation
Create a simple counter service:
defmodule Counter do
use GenServer
# Client API
def start_link(initial_value) do
GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
end
def increment do
GenServer.call(__MODULE__, :increment)
end
def get_value do
GenServer.call(__MODULE__, :get_value)
end
def reset do
GenServer.cast(__MODULE__, :reset)
end
# Server Callbacks
@impl true
def init(initial_value) do
{:ok, initial_value}
end
@impl true
def handle_call(:increment, _from, state) do
new_state = state + 1
{:reply, new_state, new_state}
end
@impl true
def handle_call(:get_value, _from, state) do
{:reply, state, state}
end
@impl true
def handle_cast(:reset, _state) do
{:noreply, 0}
end
endUsage:
{:ok, _pid} = Counter.start_link(0)
Counter.increment() # Returns 1
Counter.increment() # Returns 2
Counter.get_value() # Returns 2
Counter.reset()
Counter.get_value() # Returns 0How It Works:
start_link/1: Spawns GenServer process, callsinit/1call/2: Sends message, blocks until server replies viahandle_call/3cast/2: Sends message, returns immediately, server handles viahandle_cast/2- State flows through callbacks:
{:reply, response, new_state}or{:noreply, new_state}
Real-World GenServer: Key-Value Store
defmodule KeyValueStore do
use GenServer
# Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, %{}, opts)
end
def put(server, key, value) do
GenServer.call(server, {:put, key, value})
end
def get(server, key) do
GenServer.call(server, {:get, key})
end
def delete(server, key) do
GenServer.cast(server, {:delete, key})
end
def list_keys(server) do
GenServer.call(server, :list_keys)
end
# Server Callbacks
@impl true
def init(_opts) do
{:ok, %{}}
end
@impl true
def handle_call({:put, key, value}, _from, state) do
new_state = Map.put(state, key, value)
{:reply, :ok, new_state}
end
@impl true
def handle_call({:get, key}, _from, state) do
{:reply, Map.get(state, key), state}
end
@impl true
def handle_call(:list_keys, _from, state) do
{:reply, Map.keys(state), state}
end
@impl true
def handle_cast({:delete, key}, state) do
new_state = Map.delete(state, key)
{:noreply, new_state}
end
endUsage:
{:ok, store} = KeyValueStore.start_link()
KeyValueStore.put(store, :name, "Alice")
KeyValueStore.put(store, :age, 30)
KeyValueStore.get(store, :name) # "Alice"
KeyValueStore.list_keys(store) # [:name, :age]
KeyValueStore.delete(store, :age)
KeyValueStore.list_keys(store) # [:name]GenServer with Timeouts
Handle long-running operations with timeouts:
defmodule Worker do
use GenServer
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def process_with_timeout(data, timeout \\ 5000) do
GenServer.call(__MODULE__, {:process, data}, timeout)
end
@impl true
def init(_opts) do
{:ok, %{}}
end
@impl true
def handle_call({:process, data}, _from, state) do
# Simulate long processing
result = expensive_operation(data)
{:reply, result, state}
end
defp expensive_operation(data) do
# Simulate work
:timer.sleep(1000)
String.upcase(data)
end
endWorker.start_link([])
Worker.process_with_timeout("hello") # "HELLO" after 1 second
Worker.process_with_timeout("world", 500) # Might raise timeout errorGenServer Lifecycle Hooks
defmodule LifecycleDemo do
use GenServer
require Logger
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(opts) do
Logger.info("GenServer starting with opts: #{inspect(opts)}")
# Schedule periodic work
schedule_work()
{:ok, %{started_at: DateTime.utc_now()}}
end
@impl true
def handle_info(:work, state) do
Logger.info("Performing periodic work...")
schedule_work()
{:noreply, state}
end
@impl true
def terminate(reason, state) do
Logger.info("GenServer terminating: #{inspect(reason)}")
Logger.info("Final state: #{inspect(state)}")
:ok
end
defp schedule_work do
Process.send_after(self(), :work, 10_000) # Every 10 seconds
end
endBest Practices:
- Separate client API from callbacks: Clean interface, hide implementation
- Use
callfor queries: When you need a response - Use
castfor commands: When you don’t need confirmation - Keep callbacks fast: Long operations block the GenServer
- Handle
terminate/2: Clean up resources (close files, connections) - Use named processes sparingly: Registry is better for dynamic processes
Section 2: Supervisor - Fault Tolerance
Supervisors monitor processes and restart them when they crash.
Supervision Strategies
Three restart strategies:
- :one_for_one: Restart only crashed child
- :one_for_all: Restart all children when one crashes
- :rest_for_one: Restart crashed child and all children started after it
Basic Supervisor
defmodule MyApp.Supervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
children = [
{Counter, 0},
{KeyValueStore, []}
]
Supervisor.init(children, strategy: :one_for_one)
end
endStart the supervisor:
{:ok, _pid} = MyApp.Supervisor.start_link([])Now if Counter or KeyValueStore crashes, the supervisor restarts it automatically.
Supervision Tree Example
Build a multi-level supervision tree:
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
# Database connection pool
{MyApp.Repo, []},
# Web endpoint (Phoenix)
MyAppWeb.Endpoint,
# Business logic supervisor
MyApp.BusinessSupervisor,
# Background job supervisor
MyApp.JobSupervisor
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule MyApp.BusinessSupervisor do
use Supervisor
def start_link(opts) do
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
children = [
{UserCache, []},
{SessionManager, []},
{NotificationService, []}
]
Supervisor.init(children, strategy: :one_for_one)
end
endSupervision Tree Visualization:
Application.Supervisor (:one_for_one)
├── Repo
├── Endpoint
├── BusinessSupervisor (:one_for_one)
│ ├── UserCache
│ ├── SessionManager
│ └── NotificationService
└── JobSupervisor (:one_for_one)
├── EmailWorker
└── ReportWorkerDynamic Supervisor
Supervise dynamically created processes:
defmodule MyApp.WorkerSupervisor do
use DynamicSupervisor
def start_link(opts) do
DynamicSupervisor.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl true
def init(_opts) do
DynamicSupervisor.init(strategy: :one_for_one)
end
def start_worker(arg) do
spec = {Worker, arg}
DynamicSupervisor.start_child(__MODULE__, spec)
end
def stop_worker(pid) do
DynamicSupervisor.terminate_child(__MODULE__, pid)
end
endUsage:
{:ok, _pid} = MyApp.WorkerSupervisor.start_link([])
{:ok, worker1} = MyApp.WorkerSupervisor.start_worker("job1")
{:ok, worker2} = MyApp.WorkerSupervisor.start_worker("job2")
MyApp.WorkerSupervisor.stop_worker(worker1)Restart Strategies Comparison
defmodule StrategyDemo do
use Supervisor
# :one_for_one - independent workers
def init_one_for_one(_opts) do
children = [
{Logger1, []},
{Logger2, []},
{Logger3, []}
]
# If Logger2 crashes, only Logger2 restarts
Supervisor.init(children, strategy: :one_for_one)
end
# :one_for_all - interdependent workers
def init_one_for_all(_opts) do
children = [
{Database, []},
{Cache, []}, # Depends on Database
{WebServer, []} # Depends on Cache
]
# If any crashes, all restart (maintain consistency)
Supervisor.init(children, strategy: :one_for_all)
end
# :rest_for_one - dependent chain
def init_rest_for_one(_opts) do
children = [
{ConfigLoader, []},
{Database, []}, # Depends on ConfigLoader
{APIClient, []} # Depends on Database
]
# If Database crashes, Database and APIClient restart
# If APIClient crashes, only APIClient restarts
Supervisor.init(children, strategy: :rest_for_one)
end
endWhen to Use Each Strategy:
- :one_for_one: Default choice, independent services
- :one_for_all: Tightly coupled services that must stay in sync
- :rest_for_one: Services with start-order dependencies
Restart Intensity and Period
Prevent infinite restart loops:
defmodule MyApp.Supervisor do
use Supervisor
@impl true
def init(_opts) do
children = [
{FlakeyWorker, []}
]
# Allow 3 restarts within 5 seconds
# After that, supervisor itself crashes (escalate to parent)
Supervisor.init(children,
strategy: :one_for_one,
max_restarts: 3,
max_seconds: 5
)
end
endBest Practices:
- Design for crashes: Let it crash, supervisor handles recovery
- Use :one_for_one by default: Simplest and most common
- Limit restart intensity: Prevent infinite restart loops
- Organize by failure domain: Group related processes under supervisor
- Escalate failures: If child keeps crashing, let supervisor crash too
Section 3: Application - Project Structure
OTP Application is the standard unit of deployment.
Application Behavior
Every Mix project can be an application:
defmodule MyApp.MixProject do
use Mix.Project
def project do
[
app: :my_app,
version: "0.1.0",
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger],
mod: {MyApp.Application, []} # Application callback
]
end
defp deps do
[]
end
endApplication Callback
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
# Processes to supervise
{MyApp.Repo, []},
{MyApp.Cache, []},
MyAppWeb.Endpoint
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
@impl true
def stop(_state) do
# Cleanup on application stop
:ok
end
endApplication Configuration
Configure your application:
import Config
config :my_app,
api_key: "dev-key",
timeout: 5000
import Config
config :my_app,
api_key: System.get_env("API_KEY"),
timeout: 30000Access configuration:
api_key = Application.get_env(:my_app, :api_key)
timeout = Application.get_env(:my_app, :timeout, 5000) # Default 5000Runtime configuration (Elixir 1.11+):
import Config
if config_env() == :prod do
config :my_app,
api_key: System.fetch_env!("API_KEY"),
database_url: System.fetch_env!("DATABASE_URL")
endApplication Dependencies
Declare application dependencies:
def application do
[
extra_applications: [:logger, :crypto, :ssl],
mod: {MyApp.Application, []}
]
endApplications start in dependency order:
- Logger
- Crypto
- SSL
- MyApp
Section 4: Task and Agent - Concurrent Utilities
Task and Agent provide simpler abstractions for common patterns.
Task - Concurrent Computation
Execute work concurrently:
task = Task.async(fn ->
:timer.sleep(1000)
"Result"
end)
IO.puts("Doing other work...")
result = Task.await(task)
IO.puts(result) # "Result" after 1 secondMultiple concurrent tasks:
tasks = Enum.map(1..5, fn i ->
Task.async(fn ->
:timer.sleep(1000)
i * 2
end)
end)
results = Task.await_many(tasks)
IO.inspect(results) # [2, 4, 6, 8, 10] after 1 second (not 5)Supervised Tasks
Run tasks under supervision:
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
{Task.Supervisor, name: MyApp.TaskSupervisor}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Task.Supervisor.start_child(MyApp.TaskSupervisor, fn ->
# This task is supervised - if it crashes, supervisor handles it
perform_work()
end)
task = Task.Supervisor.async(MyApp.TaskSupervisor, fn ->
fetch_data()
end)
result = Task.await(task)Agent - Simple State Container
Agent wraps state in a process:
{:ok, agent} = Agent.start_link(fn -> %{} end)
Agent.update(agent, fn state ->
Map.put(state, :count, 1)
end)
count = Agent.get(agent, fn state ->
Map.get(state, :count)
end)
IO.puts(count) # 1
{old_count, new_count} = Agent.get_and_update(agent, fn state ->
old = Map.get(state, :count, 0)
new = old + 1
{old, Map.put(state, :count, new)}
end)Named agent:
defmodule Counter do
def start_link(initial_value) do
Agent.start_link(fn -> initial_value end, name: __MODULE__)
end
def increment do
Agent.update(__MODULE__, &(&1 + 1))
end
def get do
Agent.get(__MODULE__, & &1)
end
end
Counter.start_link(0)
Counter.increment()
Counter.increment()
Counter.get() # 2Task vs Agent vs GenServer:
- Task: One-off concurrent computation, short-lived
- Agent: Simple state storage, minimal logic
- GenServer: Complex state + behavior, long-lived
Section 5: Phoenix Framework - Web Development
Phoenix is the leading Elixir web framework.
Creating a Phoenix Project
mix archive.install hex phx_new
mix phx.new my_app
cd my_app
mix phx.serverVisit http://localhost:4000
Phoenix Project Structure
my_app/
├── lib/
│ ├── my_app/ # Business logic
│ │ ├── accounts/ # Domain context
│ │ └── repo.ex # Database repo
│ └── my_app_web/ # Web interface
│ ├── controllers/
│ ├── views/
│ ├── templates/
│ ├── router.ex
│ └── endpoint.ex
├── test/
├── config/
└── mix.exsRouting
Define routes in router.ex:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {MyAppWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
get "/", PageController, :index
get "/about", PageController, :about
resources "/users", UserController
resources "/posts", PostController, only: [:index, :show]
end
scope "/api", MyAppWeb do
pipe_through :api
get "/health", HealthController, :check
resources "/posts", API.PostController, except: [:new, :edit]
end
endControllers
Handle requests:
defmodule MyAppWeb.PageController do
use MyAppWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
def about(conn, _params) do
render(conn, "about.html", title: "About Us")
end
end
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
def index(conn, _params) do
users = Accounts.list_users()
render(conn, "index.html", users: users)
end
def show(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
render(conn, "show.html", user: user)
end
def create(conn, %{"user" => user_params}) do
case Accounts.create_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created successfully.")
|> redirect(to: Routes.user_path(conn, :show, user))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
endJSON API
defmodule MyAppWeb.API.PostController do
use MyAppWeb, :controller
def index(conn, _params) do
posts = Blog.list_posts()
render(conn, "index.json", posts: posts)
end
def show(conn, %{"id" => id}) do
post = Blog.get_post!(id)
render(conn, "show.json", post: post)
end
def create(conn, %{"post" => post_params}) do
case Blog.create_post(post_params) do
{:ok, post} ->
conn
|> put_status(:created)
|> render("show.json", post: post)
{:error, changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render(MyAppWeb.ChangesetView, "error.json", changeset: changeset)
end
end
endView:
defmodule MyAppWeb.API.PostView do
use MyAppWeb, :view
def render("index.json", %{posts: posts}) do
%{data: Enum.map(posts, &post_json/1)}
end
def render("show.json", %{post: post}) do
%{data: post_json(post)}
end
defp post_json(post) do
%{
id: post.id,
title: post.title,
body: post.body,
author: post.author,
inserted_at: post.inserted_at
}
end
endPlugs - HTTP Pipeline
Plugs are composable modules for transforming requests:
defmodule MyAppWeb.Plugs.RequireAuth do
import Plug.Conn
import Phoenix.Controller
def init(opts), do: opts
def call(conn, _opts) do
if get_session(conn, :user_id) do
conn
else
conn
|> put_flash(:error, "You must be logged in")
|> redirect(to: "/login")
|> halt()
end
end
endUse in router or controller:
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, MyAppWeb.Plugs.RequireAuth]
resources "/posts", PostController
end
defmodule MyAppWeb.AdminController do
use MyAppWeb, :controller
plug MyAppWeb.Plugs.RequireAuth when action in [:edit, :update, :delete]
# ...
endSection 6: LiveView - Real-Time Interfaces
LiveView enables real-time, interactive UIs without JavaScript.
Basic LiveView
defmodule MyAppWeb.CounterLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
@impl true
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
@impl true
def handle_event("decrement", _params, socket) do
{:noreply, update(socket, :count, &(&1 - 1))}
end
@impl true
def render(assigns) do
~H"""
<div>
<h1>Counter: <%= @count %></h1>
<button phx-click="increment">+</button>
<button phx-click="decrement">-</button>
</div>
"""
end
endAdd to router:
scope "/", MyAppWeb do
pipe_through :browser
live "/counter", CounterLive
endLiveView Form Handling
defmodule MyAppWeb.SearchLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, query: "", results: [])}
end
@impl true
def handle_event("search", %{"query" => query}, socket) do
results = perform_search(query)
{:noreply, assign(socket, query: query, results: results)}
end
@impl true
def render(assigns) do
~H"""
<div>
<form phx-submit="search">
<input type="text" name="query" value={@query}
placeholder="Search..." phx-debounce="300" />
<button type="submit">Search</button>
</form>
<ul>
<%= for result <- @results do %>
<li><%= result.title %></li>
<% end %>
</ul>
</div>
"""
end
defp perform_search(query) do
# Simulate search
[
%{title: "Result 1 for #{query}"},
%{title: "Result 2 for #{query}"}
]
end
endLiveView with Streams (Phoenix 1.7+)
Efficiently handle large lists:
defmodule MyAppWeb.PostsLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
posts = Blog.list_posts()
{:ok, stream(socket, :posts, posts)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
post = Blog.get_post!(id)
{:ok, _} = Blog.delete_post(post)
{:noreply, stream_delete(socket, :posts, post)}
end
@impl true
def render(assigns) do
~H"""
<div>
<h1>Posts</h1>
<div id="posts" phx-update="stream">
<%= for {dom_id, post} <- @streams.posts do %>
<div id={dom_id}>
<h2><%= post.title %></h2>
<button phx-click="delete" phx-value-id={post.id}>Delete</button>
</div>
<% end %>
</div>
</div>
"""
end
endLiveView PubSub
Real-time updates across clients:
defmodule MyAppWeb.ChatLive do
use MyAppWeb, :live_view
@impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "chat:lobby")
end
messages = Chat.list_messages()
{:ok, assign(socket, messages: messages, message: "")}
end
@impl true
def handle_event("send", %{"message" => message}, socket) do
Chat.create_message(%{body: message, user: "User"})
Phoenix.PubSub.broadcast(MyApp.PubSub, "chat:lobby", {:new_message, message})
{:noreply, assign(socket, message: "")}
end
@impl true
def handle_info({:new_message, message}, socket) do
{:noreply, update(socket, :messages, fn messages ->
[message | messages]
end)}
end
@impl true
def render(assigns) do
~H"""
<div>
<div id="messages">
<%= for message <- @messages do %>
<div><%= message %></div>
<% end %>
</div>
<form phx-submit="send">
<input type="text" name="message" value={@message} />
<button type="submit">Send</button>
</form>
</div>
"""
end
endSection 7: Ecto - Database Layer
Ecto is Elixir’s database wrapper and query generator.
Defining Schemas
defmodule MyApp.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
field :confirmed, :boolean, default: false
timestamps()
end
def changeset(user, attrs) do
user
|> cast(attrs, [:name, :email, :age, :confirmed])
|> validate_required([:name, :email])
|> validate_format(:email, ~r/@/)
|> validate_number(:age, greater_than: 0, less_than: 150)
|> unique_constraint(:email)
end
endMigrations
defmodule MyApp.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string, null: false
add :email, :string, null: false
add :age, :integer
add :confirmed, :boolean, default: false
timestamps()
end
create unique_index(:users, [:email])
end
endRun migrations:
mix ecto.migrate
mix ecto.rollbackQuerying
import Ecto.Query
alias MyApp.Accounts.User
alias MyApp.Repo
users = Repo.all(User)
user = Repo.get(User, 1)
user = Repo.get!(User, 1) # Raises if not found
user = Repo.get_by(User, email: "alice@example.com")
query = from u in User,
where: u.age > 18,
select: u
adults = Repo.all(query)
adults = User
|> where([u], u.age > 18)
|> order_by([u], desc: u.inserted_at)
|> limit(10)
|> Repo.all()
count = User
|> where([u], u.confirmed == true)
|> Repo.aggregate(:count)
{count, _} = User
|> where([u], u.age < 18)
|> Repo.update_all(set: [confirmed: false])Changesets and Validation
changeset = User.changeset(%User{}, %{
name: "Alice",
email: "alice@example.com",
age: 30
})
case Repo.insert(changeset) do
{:ok, user} ->
IO.puts("User created: #{user.id}")
{:error, changeset} ->
IO.inspect(changeset.errors)
end
user = Repo.get!(User, 1)
changeset = User.changeset(user, %{age: 31})
Repo.update(changeset)
user = Repo.get!(User, 1)
Repo.delete(user)Associations
defmodule MyApp.Blog.Post do
use Ecto.Schema
schema "posts" do
field :title, :string
field :body, :text
belongs_to :author, MyApp.Accounts.User
timestamps()
end
end
defmodule MyApp.Accounts.User do
use Ecto.Schema
schema "users" do
field :name, :string
has_many :posts, MyApp.Blog.Post, foreign_key: :author_id
timestamps()
end
endPreloading associations:
users = Repo.all(User)
Enum.each(users, fn user ->
posts = Repo.preload(user, :posts).posts
IO.inspect(posts)
end)
users = User
|> preload(:posts)
|> Repo.all()
users = User
|> join(:inner, [u], p in assoc(u, :posts))
|> where([u, p], p.published == true)
|> preload([u, p], posts: p)
|> Repo.all()Section 8: Testing Strategies
Comprehensive testing for OTP applications.
ExUnit Basics
defmodule MyApp.AccountsTest do
use ExUnit.Case, async: true
alias MyApp.Accounts
describe "create_user/1" do
test "creates user with valid attributes" do
attrs = %{name: "Alice", email: "alice@example.com", age: 30}
assert {:ok, user} = Accounts.create_user(attrs)
assert user.name == "Alice"
assert user.email == "alice@example.com"
end
test "returns error with invalid email" do
attrs = %{name: "Alice", email: "invalid", age: 30}
assert {:error, changeset} = Accounts.create_user(attrs)
assert %{email: ["has invalid format"]} = errors_on(changeset)
end
end
endTesting GenServers
defmodule CounterTest do
use ExUnit.Case
setup do
{:ok, pid} = Counter.start_link(0)
%{counter: pid}
end
test "increments counter", %{counter: counter} do
assert Counter.increment(counter) == 1
assert Counter.increment(counter) == 2
end
test "resets counter", %{counter: counter} do
Counter.increment(counter)
Counter.increment(counter)
Counter.reset(counter)
assert Counter.get_value(counter) == 0
end
endTesting with Mocks
defp deps do
[
{:mox, "~> 1.0", only: :test}
]
end
defmodule MyApp.HTTPClient do
@callback get(url :: String.t()) :: {:ok, map()} | {:error, term()}
end
defmodule MyApp.HTTPClient.HTTPoison do
@behaviour MyApp.HTTPClient
def get(url) do
case HTTPoison.get(url) do
{:ok, %{body: body}} -> {:ok, Jason.decode!(body)}
error -> error
end
end
end
Mox.defmock(MyApp.HTTPClient.Mock, for: MyApp.HTTPClient)
defmodule MyApp.APITest do
use ExUnit.Case, async: true
import Mox
setup :verify_on_exit!
test "fetches data from API" do
expect(MyApp.HTTPClient.Mock, :get, fn _url ->
{:ok, %{"data" => "test"}}
end)
assert {:ok, %{"data" => "test"}} = MyApp.API.fetch_data()
end
endTesting Phoenix Controllers
defmodule MyAppWeb.PageControllerTest do
use MyAppWeb.ConnCase
test "GET /", %{conn: conn} do
conn = get(conn, "/")
assert html_response(conn, 200) =~ "Welcome"
end
end
defmodule MyAppWeb.UserControllerTest do
use MyAppWeb.ConnCase
describe "create user" do
test "redirects when data is valid", %{conn: conn} do
attrs = %{name: "Alice", email: "alice@example.com"}
conn = post(conn, Routes.user_path(conn, :create), user: attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == Routes.user_path(conn, :show, id)
end
test "renders errors when data is invalid", %{conn: conn} do
attrs = %{name: "", email: "invalid"}
conn = post(conn, Routes.user_path(conn, :create), user: attrs)
assert html_response(conn, 200) =~ "Oops, something went wrong!"
end
end
endTesting LiveView
defmodule MyAppWeb.CounterLiveTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
test "increments counter", %{conn: conn} do
{:ok, view, html} = live(conn, "/counter")
assert html =~ "Counter: 0"
assert view |> element("button", "+") |> render_click() =~ "Counter: 1"
assert view |> element("button", "+") |> render_click() =~ "Counter: 2"
end
test "decrements counter", %{conn: conn} do
{:ok, view, _html} = live(conn, "/counter")
view |> element("button", "+") |> render_click()
view |> element("button", "+") |> render_click()
assert view |> element("button", "-") |> render_click() =~ "Counter: 1"
end
endSection 9: Configuration Management
Manage configuration across environments.
Compile-Time Configuration
import Config
config :my_app,
api_endpoint: "https://api.example.com",
timeout: 5000,
retry_attempts: 3
import_config "#{config_env()}.exs"import Config
config :my_app,
api_endpoint: "http://localhost:4000",
timeout: 30000import Config
config :my_app,
timeout: 10000Runtime Configuration
import Config
if config_env() == :prod do
config :my_app,
api_key: System.fetch_env!("API_KEY"),
database_url: System.fetch_env!("DATABASE_URL"),
secret_key_base: System.fetch_env!("SECRET_KEY_BASE")
endAccessing Configuration
defmodule MyApp.API do
@api_endpoint Application.compile_env(:my_app, :api_endpoint)
@timeout Application.compile_env(:my_app, :timeout, 5000)
def fetch_data do
# Use compile-time config
HTTPoison.get(@api_endpoint, [], recv_timeout: @timeout)
end
def fetch_with_runtime_key do
# Use runtime config
api_key = Application.get_env(:my_app, :api_key)
HTTPoison.get("#{@api_endpoint}?key=#{api_key}")
end
endSection 10: Production Patterns
Patterns for production-ready applications.
Graceful Degradation
defmodule MyApp.ResilientAPI do
require Logger
def fetch_data(url, opts \\ []) do
timeout = Keyword.get(opts, :timeout, 5000)
retries = Keyword.get(opts, :retries, 3)
fetch_with_retry(url, timeout, retries)
end
defp fetch_with_retry(url, timeout, retries) when retries > 0 do
case HTTPoison.get(url, [], recv_timeout: timeout) do
{:ok, %{status_code: 200, body: body}} ->
{:ok, body}
{:ok, %{status_code: status}} ->
Logger.warn("API returned #{status} for #{url}")
{:error, :bad_status}
{:error, reason} ->
Logger.warn("API request failed: #{inspect(reason)}, retries left: #{retries - 1}")
:timer.sleep(1000)
fetch_with_retry(url, timeout, retries - 1)
end
end
defp fetch_with_retry(_url, _timeout, 0) do
{:error, :max_retries}
end
endCircuit Breaker Pattern
defmodule MyApp.CircuitBreaker do
use GenServer
defstruct [:failures, :state, :threshold, :timeout]
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def call(fun) do
case GenServer.call(__MODULE__, :get_state) do
:open ->
{:error, :circuit_open}
:closed ->
execute_and_track(fun)
end
end
@impl true
def init(opts) do
state = %__MODULE__{
failures: 0,
state: :closed,
threshold: Keyword.get(opts, :threshold, 5),
timeout: Keyword.get(opts, :timeout, 60_000)
}
{:ok, state}
end
@impl true
def handle_call(:get_state, _from, state) do
{:reply, state.state, state}
end
defp execute_and_track(fun) do
case fun.() do
{:ok, result} ->
GenServer.cast(__MODULE__, :success)
{:ok, result}
error ->
GenServer.cast(__MODULE__, :failure)
error
end
end
@impl true
def handle_cast(:success, state) do
{:noreply, %{state | failures: 0, state: :closed}}
end
@impl true
def handle_cast(:failure, state) do
new_failures = state.failures + 1
if new_failures >= state.threshold do
Process.send_after(self(), :half_open, state.timeout)
{:noreply, %{state | failures: 0, state: :open}}
else
{:noreply, %{state | failures: new_failures}}
end
end
@impl true
def handle_info(:half_open, state) do
{:noreply, %{state | state: :closed}}
end
endHealth Checks
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def check(conn, _params) do
health = %{
status: "ok",
database: check_database(),
cache: check_cache(),
external_api: check_external_api()
}
overall_status = if all_healthy?(health), do: 200, else: 503
conn
|> put_status(overall_status)
|> json(health)
end
defp check_database do
try do
MyApp.Repo.query!("SELECT 1")
"healthy"
rescue
_ -> "unhealthy"
end
end
defp check_cache do
case MyApp.Cache.get(:health_check) do
{:ok, _} -> "healthy"
_ -> "unhealthy"
end
end
defp check_external_api do
case MyApp.ExternalAPI.ping() do
{:ok, _} -> "healthy"
_ -> "degraded"
end
end
defp all_healthy?(health) do
Enum.all?(Map.values(health), &(&1 == "healthy" or &1 == "ok"))
end
endTelemetry and Metrics
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
# Telemetry supervisor
MyAppWeb.Telemetry,
# Other children...
MyApp.Repo,
MyAppWeb.Endpoint
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
defmodule MyAppWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
def init(_arg) do
children = [
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
]
Supervisor.init(children, strategy: :one_for_one)
end
def metrics do
[
# Phoenix metrics
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),
# Database metrics
summary("my_app.repo.query.total_time", unit: {:native, :millisecond}),
summary("my_app.repo.query.decode_time", unit: {:native, :millisecond}),
summary("my_app.repo.query.query_time", unit: {:native, :millisecond}),
# VM metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io")
]
end
defp periodic_measurements do
[]
end
endRelated Content
Previous Tutorials:
- Beginner Tutorial - Elixir fundamentals
- Quick Start - Core concepts
Next Steps:
- Advanced Tutorial - Expert topics
How-To Guides:
- Elixir Cookbook - Practical recipes
- How to Build GenServer - GenServer patterns
- How to Build Supervisor Tree - Supervision strategies
- How to Use Phoenix Framework - Phoenix guide
- How to Use Ecto - Database patterns
- How to Test OTP Applications - Testing OTP
Explanations:
- Best Practices - Production standards
- Anti-Patterns - OTP pitfalls
Reference:
- Elixir Cheat Sheet - Syntax guide
- Elixir Glossary - OTP terminology
Next Steps
Master These Concepts:
- GenServer: Practice building stateful services
- Supervisor: Design fault-tolerant systems
- Phoenix: Build complete web applications
- Ecto: Master database interactions
Continue Learning:
- Advanced Tutorial - BEAM VM, distributed systems, metaprogramming
- How-To Guides - Practical patterns and solutions
- Cookbook - Ready-to-use recipes
Practice Projects:
- Chat Application: Phoenix + LiveView + PubSub
- Task Queue: GenServer + Supervisor + Ecto
- REST API: Phoenix + Ecto + Authentication
- Monitoring Dashboard: LiveView + Telemetry + Charts
Resources:
You now have the foundation for production Elixir development!