
- What is OTP?
- The OTP Philosophy
- GenServer: The Basic Building Block
- Why use GenServer?
- Understanding the Callbacks
- When to Use Call vs Cast
- Supervisors: The Safety Net
- Why Supervision Matters
- Basic Supervisor
- Supervisor Strategies
- Building Web Applications with Phoenix
- Why Phoenix?
- Creating a Phoenix Application
- Understanding Phoenix Structure
- Routing in Phoenix
- Controllers
- LiveView for Real-time Features
- Working with Databases using Ecto
- Why Ecto?
- Setting up Ecto
- Schema Definition
- Repository Operations
- Deploying Elixir Applications
- Creating Releases
- Environment Configuration
- What's Next?
- Practice Exercises
- Key Points
OTP and Building Real Applications - Learn Elixir Part 4
In this part, we will look at OTP (Open Telecom Platform), which is an important part of Elixir. OTP helps you build applications that can handle many users at the same time, recover from errors, and run on more than one machine.
What is OTP?
OTP is a complete framework with tools and patterns for building reliable applications. It was first made by Ericsson for telecom systems, but now anyone using Elixir can use it.
The main idea behind OTP is that you cannot stop all failures, but you can make your system recover from them. If something goes wrong, OTP helps your application notice the problem and fix itself.
The OTP Philosophy
OTP is based on a few main ideas:
- Process Isolation: Each part of your program runs in its own process.
- Supervision: Processes are watched and restarted if they fail.
- Message Passing: Processes talk to each other by sending messages, not by sharing memory.
- Fault Tolerance: Problems are kept in one place and do not crash the whole system.
OTP gives you tools to use these ideas without too much extra work.
GenServer: The Basic Building Block
GenServer is the OTP feature you will use most often. It gives you a standard way to create processes that keep state and answer messages. You can think of it as a pattern for building services that need to remember things.
Why use GenServer?
Without GenServer, you would need to write all the code for message passing, state, and error handling yourself. GenServer gives you a simple interface and takes care of the hard parts.
Here's what a basic GenServer looks like:
defmodule Counter do
  use GenServer
  # Client API - what other processes call
  def start_link(initial_value \\ 0) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end
  def increment do
    GenServer.call(__MODULE__, :increment)
  end
  def get_count do
    GenServer.call(__MODULE__, :get_count)
  end
  # Server callbacks - what the GenServer does internally
  @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_count, _from, state) do
    {:reply, state, state}
  end
end
The key insight is that GenServer separates the client interface (what other processes call) from the server implementation (how the state is managed). The @impl true annotations tell the compiler that these are GenServer callbacks.
Understanding the Callbacks
Let's break down what each callback does:
- init/1: Called when the GenServer starts. Returns- {:ok, initial_state}
- handle_call/3: Handles synchronous requests (calls). Returns- {:reply, response, new_state}
- handle_cast/2: Handles asynchronous requests (casts). Returns- {:noreply, new_state}
- handle_info/2: Handles other messages (like from- send/2). Returns- {:noreply, new_state}
The state flows through these callbacks, and each one can transform it as needed.
When to Use Call vs Cast
- GenServer.call/2: Synchronous - the caller waits for a response
- GenServer.cast/2: Asynchronous - the caller doesn't wait
Use call when you need a response, cast when you just want to send a message and continue.
Supervisors: The Safety Net
Supervisors are what make Elixir applications fault-tolerant. They monitor child processes and restart them when they fail. Think of them as the parent process that watches over their children.
Why Supervision Matters
In traditional applications, if a component fails, it might bring down the entire system. With supervisors, failures are isolated and handled automatically. If a worker process crashes, the supervisor restarts it. If it keeps crashing, the supervisor can try different strategies.
Basic Supervisor
defmodule MyApp.Supervisor do
  use Supervisor
  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end
  @impl true
  def init(_init_arg) do
    children = [
      # Start the counter
      {Counter, 0},
      
      # Start a worker with specific options
      {ChatRoom, "general"}
    ]
    Supervisor.init(children, strategy: :one_for_one)
  end
end
The supervisor defines a list of child processes to start and monitor. Each child is specified as a tuple {module, arguments}.
Supervisor Strategies
Supervisors have different strategies for handling failures:
- :one_for_one: Restart only the failed child
- :one_for_all: Restart all children if any child fails
- :rest_for_one: Restart the failed child and all children started after it
- :simple_one_for_one: Restart only the failed child (for dynamic children)
Choose the strategy based on how your processes depend on each other. If they're independent, use :one_for_one. If they need to be consistent, use :one_for_all.
Building Web Applications with Phoenix
Phoenix is Elixir's web framework, and it's designed for high-performance real-time applications, built on the same principles as OTP.
Why Phoenix?
Phoenix is fast and it can handle thousands of concurrent connections with minimal resource usage. It's also great for real-time features like chat, live updates, and collaborative editing.
Creating a Phoenix Application
# Install Phoenix
mix archive.install hex phx_new
# Create a new Phoenix application
mix phx.new my_app --no-ecto
# Create with database support
mix phx.new my_app --database postgres
The --no-ecto flag creates a Phoenix app without database support, which is good for learning. The --database postgres flag sets up PostgreSQL integration.
Understanding Phoenix Structure
A Phoenix application has a clear structure:
my_app/
├── lib/
│   ├── my_app/          # Your business logic
│   │   ├── application.ex
│   │   └── ...
│   └── my_app_web/      # Web-specific code
│       ├── endpoint.ex
│       ├── router.ex
│       ├── controllers/
│       └── templates/
├── config/              # Configuration files
├── test/                # Tests
└── mix.exs
The separation between my_app and my_app_web is important - it keeps your business logic separate from your web interface.
Routing in Phoenix
Phoenix routing is powerful and flexible:
defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MyAppWeb.Layouts, :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, :home
    resources "/users", UserController
  end
  scope "/api", MyAppWeb do
    pipe_through :api
    resources "/users", UserController, only: [:index, :show, :create]
  end
end
Pipelines are groups of plugs (middleware) that process requests. The :browser pipeline handles HTML requests, while the :api pipeline handles JSON requests.
Controllers
Controllers handle HTTP requests and return responses:
defmodule MyAppWeb.UserController do
  use MyAppWeb, :controller
  def index(conn, _params) do
    users = [
      %{id: 1, name: "John", email: "john@example.com"},
      %{id: 2, name: "Jane", email: "jane@example.com"}
    ]
    
    render(conn, :index, users: users)
  end
  def show(conn, %{"id" => id}) do
    user = %{id: id, name: "John", email: "john@example.com"}
    render(conn, :show, user: user)
  end
end
The conn parameter is the connection struct that contains all the request and response data. The params parameter contains the request parameters.
LiveView for Real-time Features
LiveView is one of Phoenix's most powerful features. It enables real-time, server-rendered HTML without writing JavaScript:
defmodule MyAppWeb.ChatLive do
  use MyAppWeb, :live_view
  def mount(_params, _session, socket) do
    if connected?(socket) do
      Phoenix.PubSub.subscribe(MyApp.PubSub, "chat:general")
    end
    {:ok, assign(socket, messages: [], current_user: "Anonymous")}
  end
  def handle_event("send_message", %{"message" => message}, socket) do
    if String.trim(message) != "" do
      Phoenix.PubSub.broadcast(
        MyApp.PubSub,
        "chat:general",
        {:new_message, socket.assigns.current_user, message}
      )
    end
    {:noreply, socket}
  end
  def handle_info({:new_message, user, message}, socket) do
    new_message = %{
      id: :rand.uniform(1000),
      user: user,
      message: message,
      timestamp: DateTime.utc_now()
    }
    {:noreply, update(socket, :messages, fn messages -> [new_message | messages] end)}
  end
end
LiveView maintains a persistent connection between the browser and server. When the server state changes, it automatically updates the browser. This makes real-time features much easier to implement.
Working with Databases using Ecto
Ecto is Elixir's database wrapper and query generator. It provides a clean, functional interface for working with databases.
Why Ecto?
Ecto gives you:
- Type safety: Schemas define the structure of your data
- Query building: Functional query interface
- Changesets: Data validation and transformation
- Migrations: Database schema management
- Multiple databases: Support for PostgreSQL, MySQL, SQLite, and more
Setting up Ecto
First, add Ecto to your dependencies:
# mix.exs
defp deps do
  [
    {:ecto_sql, "~> 3.10"},
    {:postgrex, ">= 0.0.0"}
  ]
end
Then configure your database:
# config/dev.exs
config :my_app, MyApp.Repo,
  username: "postgres",
  password: "postgres",
  hostname: "localhost",
  database: "my_app_dev",
  stacktrace: true,
  show_sensitive_data_on_connection_error: true,
  pool_size: 10
Schema Definition
Schemas define the structure of your data:
defmodule MyApp.Users.User do
  use Ecto.Schema
  import Ecto.Changeset
  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer
    field :is_active, :boolean, default: true
    timestamps()
  end
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :age, :is_active])
    |> validate_required([:name, :email])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0)
    |> unique_constraint(:email)
  end
end
The schema macro defines the database table structure, while the changeset function handles validation and data transformation.
Repository Operations
The repository is your interface to the database:
defmodule MyApp.Users do
  import Ecto.Query
  alias MyApp.Repo
  alias MyApp.Users.User
  def list_users do
    Repo.all(User)
  end
  def get_user!(id) do
    Repo.get!(User, id)
  end
  def create_user(attrs \\ %{}) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end
  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end
  # Complex queries
  def active_users do
    User
    |> where([u], u.is_active == true)
    |> order_by([u], u.name)
    |> Repo.all()
  end
end
The repository provides functions like all/1, get!/2, insert/1, update/1, and delete/1 for basic operations. For complex queries, you use the query interface.
Deploying Elixir Applications
Elixir makes deployment straightforward with releases. A release packages your application with the Erlang runtime, making it self-contained.
Creating Releases
# Create a release
mix release
# Create a release with hot code updates
mix release --overwrite
# Run the release
_build/prod/rel/my_app/bin/my_app start
Releases are great because:
- They're self-contained (no need to install Elixir on the server)
- They support hot code updates (update without downtime)
- They're optimized for production
Environment Configuration
Configure your application for different environments:
# 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("HOST"), port: String.to_integer(System.get_env("PORT") || "4000")],
    http: [
      port: String.to_integer(System.get_env("PORT") || "4000")
    ],
    secret_key_base: System.get_env("SECRET_KEY_BASE")
end
The runtime.exs file is evaluated at runtime, allowing you to use environment variables for configuration.
What's Next?
You're getting close to mastering Elixir! In the final article, we'll explore:
- Advanced OTP patterns and behaviors
- Distributed Elixir and clustering
- Building microservices with Elixir
- Best practices and design patterns
- Real-world project examples
Practice Exercises
Try these to reinforce what you've learned:
- GenServer: Create a simple key-value store using GenServer
- Supervisor: Build a supervisor that manages multiple worker processes
- Phoenix: Create a simple blog application with CRUD operations
- Ecto: Implement a user management system with database persistence
Key Points
- OTP provides robust abstractions for building concurrent, fault-tolerant applications
- GenServer is the foundation for stateful processes in Elixir
- Supervisors ensure your applications can recover from failures
- Phoenix enables high-performance, real-time web applications
- Ecto provides a powerful and flexible database abstraction
- Elixir releases make deployment straightforward and reliable
- The combination of OTP and functional programming makes Elixir ideal for distributed systems
- What is OTP?
- The OTP Philosophy
- GenServer: The Basic Building Block
- Why use GenServer?
- Understanding the Callbacks
- When to Use Call vs Cast
- Supervisors: The Safety Net
- Why Supervision Matters
- Basic Supervisor
- Supervisor Strategies
- Building Web Applications with Phoenix
- Why Phoenix?
- Creating a Phoenix Application
- Understanding Phoenix Structure
- Routing in Phoenix
- Controllers
- LiveView for Real-time Features
- Working with Databases using Ecto
- Why Ecto?
- Setting up Ecto
- Schema Definition
- Repository Operations
- Deploying Elixir Applications
- Creating Releases
- Environment Configuration
- What's Next?
- Practice Exercises
- Key Points