Learn Elixir: the best books to read in order
This curriculum takes you from zero Elixir knowledge to production-ready Phoenix applications and deep OTP/concurrency mastery. Each stage builds directly on the last: you first internalize functional thinking and Elixir syntax, then tackle real-world web development with Phoenix, and finally go deep into the concurrent, fault-tolerant internals that make Elixir uniquely powerful.
Functional Foundations & Elixir Basics
BeginnerUnderstand functional programming principles, Elixir syntax, pattern matching, recursion, and the core data structures — the essential vocabulary for everything that follows.
▸ Study plan for this stage
Pace: 4–5 weeks, ~40–50 pages/day. Start with "Programming Elixir ≥ 1.6" (Chapters 1–10, ~2.5 weeks), then transition to "Elixir in Action" (Chapters 1–4, ~2 weeks) for deeper context and practical patterns.
- Functional programming paradigm: immutability, first-class functions, and avoiding side effects as core design principles
- Pattern matching: the unified mechanism for destructuring, control flow, and function dispatch in Elixir
- Recursion and tail-call optimization: how to replace loops and build efficient iterative processes
- Core data structures: lists, tuples, maps, and strings—their trade-offs and idiomatic usage patterns
- Pipe operator (|>) and function composition: reading and writing data transformations as readable chains
- Anonymous functions, higher-order functions, and closures: treating functions as first-class values
- The Elixir module system and function definitions: organizing code with named functions and guards
- Atoms, pattern matching in function heads, and the match operator (=): Elixir's approach to control flow and data binding
- Explain the core difference between imperative and functional programming, and why immutability matters in Elixir.
- How does pattern matching work in Elixir, and how is it used for both data destructuring and control flow?
- Write a recursive function that processes a list without using built-in iteration functions. Why is tail-call optimization important?
- Compare lists, tuples, and maps: when would you use each, and what are their performance characteristics?
- How does the pipe operator (|>) improve code readability, and how does it relate to function composition?
- What is the difference between anonymous functions and named functions? When would you use each?
- Work through all code examples in 'Programming Elixir' Chapters 1–5 in the Elixir REPL; modify them to deepen understanding (e.g., change function parameters, test edge cases).
- Implement a recursive function to calculate factorial, sum a list, and reverse a list—then rewrite using Enum module functions to see the difference.
- Build a simple data structure (e.g., a person record using maps/tuples) and write pattern-matching functions to extract and transform its fields.
- Create a module with 5–6 small functions that use guards, multiple clauses, and pattern matching in function heads; test each with various inputs.
- Chain 3–4 data transformations using the pipe operator (e.g., parse a list of strings, filter, map, and sort) and compare readability to nested function calls.
- Write a higher-order function that takes another function as an argument and applies it to a collection; use both named and anonymous functions as arguments.
Next up: This stage equips you with Elixir's syntax and functional building blocks; the next stage will apply these foundations to concurrent programming, process management, and the actor model—where immutability and recursion become essential for writing reliable, scalable systems.

The canonical starting point for Elixir — Dave Thomas covers the language from scratch with a functional-first mindset, making it the ideal first book for any Elixir learner.

Reinforces and deepens the fundamentals while introducing processes and the BEAM runtime early, bridging the gap between pure syntax and real Elixir thinking.
Web Development with Phoenix
IntermediateBuild full-featured web applications using the Phoenix framework, understanding its MVC structure, Ecto for databases, channels for real-time features, and LiveView for interactive UIs.
▸ Study plan for this stage
Pace: 8–10 weeks, ~40–50 pages/day (mix of reading and hands-on coding)
- LiveView's stateful server-side rendering model and how it eliminates the need for JavaScript for many interactive features
- The component lifecycle in LiveView: mount, render, handle_event, and handle_info
- Building real-time features with Phoenix Channels and integrating them with LiveView
- Ecto's query API, changesets, and validations for robust data handling
- The relationship between Ecto schemas, migrations, and the database layer in a Phoenix application
- MVC architecture in Phoenix: routers, controllers, views, and how LiveView fits into this pattern
- Form handling and two-way data binding in LiveView
- Testing strategies for LiveView components and Ecto queries
- How does LiveView's server-side rendering model differ from traditional client-side JavaScript frameworks, and what are the trade-offs?
- Explain the lifecycle of a LiveView component: what happens during mount, render, and when a user event is handled?
- How do you use Ecto changesets to validate and transform user input before persisting to the database?
- What is the role of Phoenix Channels in real-time communication, and how can you integrate channels with LiveView?
- How do you structure a multi-step form or complex UI interaction using LiveView's stateful approach?
- What are the key differences between Ecto queries using the query API versus raw SQL, and when would you use each?
- Build a simple real-time counter or todo list app using LiveView, implementing mount, render, and handle_event callbacks
- Create a multi-step form in LiveView with validation at each step using Ecto changesets
- Write Ecto migrations and schemas for a small project (e.g., blog with posts and comments), then query them using the Ecto query API
- Implement a presence feature using Phoenix Channels to show which users are currently viewing a page
- Build a search feature in LiveView that filters results in real-time as the user types, backed by Ecto queries
- Create a LiveView component that manages its own state and can be reused across multiple pages in your application
- Write tests for a LiveView component covering mount, render, and event handling scenarios
- Implement a real-time notification system using Channels and LiveView that updates the UI when events occur
Next up: This stage equips you with the core skills to build interactive, data-driven web applications in Phoenix; the next stage will likely deepen your expertise in deployment, performance optimization, advanced testing patterns, and architectural decisions for scaling production systems.

The most modern and practical Phoenix book, covering both the core framework and LiveView — read this first in the web stage to get a complete, up-to-date picture of Phoenix development.

Ecto is Phoenix's database layer and deserves dedicated study; this book goes far deeper than any Phoenix overview, giving you mastery of queries, changesets, and data modeling.
Concurrency, OTP & Fault Tolerance
IntermediateDeeply understand Elixir processes, GenServer, Supervisors, and the OTP framework to design robust, concurrent, and self-healing systems.
▸ Study plan for this stage
Pace: 8–10 weeks, ~40–50 pages/day (mix of reading and hands-on coding)
- Elixir processes as lightweight, isolated units of concurrency with message-passing communication
- GenServer behavior module for building stateful, request-response concurrent components
- Supervisor trees and hierarchical fault tolerance: letting processes fail and restart automatically
- OTP principles: isolation, fault tolerance, and the 'let it crash' philosophy
- Linking and monitoring processes to detect failures and trigger recovery
- State management and message handling patterns in concurrent systems
- Building production-ready systems with OTP abstractions rather than raw processes
- Designing systems that self-heal through supervisor restart strategies
- What is a process in Elixir, and how does message passing differ from shared memory concurrency?
- When and why would you use GenServer instead of spawning raw processes?
- How do Supervisors enable fault tolerance, and what are the different restart strategies?
- What is the difference between linking and monitoring a process, and when would you use each?
- How does the OTP framework guide you to design systems that are robust and maintainable?
- What does 'let it crash' mean, and how does it change the way you think about error handling?
- Build a simple counter GenServer that handles increment/decrement messages and maintains state across calls
- Create a supervisor that manages multiple worker processes and experiment with different restart strategies (one_for_one, one_for_all, rest_for_one)
- Implement a linked process pair where killing one process triggers the other to crash, then modify it to use monitoring instead
- Design a small OTP application with a root supervisor, multiple GenServers, and a simple CLI to interact with them
- Write a GenServer that processes a queue of tasks, handles failures gracefully, and logs all state transitions
- Build a fault-tolerance test: deliberately crash a supervised process and verify that the supervisor restarts it correctly
Next up: This stage equips you with the mental models and patterns to build concurrent, self-healing systems; the next stage will likely deepen your ability to scale these systems, handle distribution across multiple nodes, and manage complex state and communication patterns in production environments.

A gentle but thorough introduction to OTP concepts — GenServers, Supervisors, and application design — making it the perfect first step into the concurrency stage.

Focuses on how to architect larger Elixir systems using OTP patterns and layers, building directly on the foundational OTP knowledge from the previous book.
Advanced Elixir & Production Mastery
ExpertMaster metaprogramming, performance, distributed systems, and the craft of writing idiomatic, production-grade Elixir at scale.
▸ Study plan for this stage
Pace: 8–10 weeks, ~40–50 pages/day (with 2–3 days per week for hands-on exercises and projects)
- Macros, compile-time code generation, and quote/unquote mechanics in Elixir
- Domain-specific languages (DSLs) and metaprogramming patterns for reducing boilerplate
- Performance profiling, optimization strategies, and benchmarking in production Elixir
- Building robust GraphQL APIs with Absinthe: schema design, resolvers, and middleware
- Subscriptions, real-time data, and WebSocket handling in GraphQL
- Error handling, validation, and authentication patterns in production GraphQL services
- Distributed systems concepts: supervision trees, fault tolerance, and scaling Elixir applications
- Idiomatic Elixir patterns: code organization, testing strategies, and maintainability at scale
- What is the difference between quote and unquote, and when would you use each in macro development?
- How can you use macros to build a domain-specific language (DSL) that reduces boilerplate in your application?
- What are the key performance bottlenecks in Elixir applications, and what tools and techniques can you use to identify and fix them?
- How do you design a GraphQL schema in Absinthe that is both flexible and maintainable, and what role do resolvers play?
- How would you implement real-time features in a GraphQL API using Absinthe subscriptions, and what are the architectural considerations?
- What patterns and best practices should you follow when building authentication, authorization, and error handling into a production GraphQL service?
- How do you structure a distributed Elixir application to handle failures gracefully and scale horizontally?
- Build a custom macro that generates CRUD functions for a given module, reducing boilerplate code by at least 50%.
- Create a small DSL using quote/unquote that allows developers to define validation rules in a readable, declarative syntax.
- Profile an existing Elixir application using tools like :fprof or :eprof; identify at least one bottleneck and implement an optimization.
- Design and implement a GraphQL schema for a real-world domain (e.g., e-commerce, social media) using Absinthe, including at least 5 types and 10 fields.
- Implement a GraphQL mutation with nested input types, custom error handling, and validation; ensure it returns meaningful error messages.
- Build a GraphQL subscription that pushes real-time updates to multiple clients; test with a WebSocket client.
- Implement middleware in Absinthe to handle authentication, rate limiting, or request logging across multiple resolvers.
- Refactor a monolithic GraphQL resolver into smaller, testable functions following idiomatic Elixir patterns; write comprehensive tests.
Next up: This stage equips you with the advanced metaprogramming and production-grade API design skills needed to architect complex, scalable systems; the next stage will likely focus on deploying, monitoring, and maintaining these systems in real-world environments at enterprise scale.

Written by the creator of Phoenix, this book unlocks Elixir's macro system — essential for understanding how Phoenix and many libraries work under the hood, and for writing expressive DSLs.

A real-world capstone that applies advanced Elixir and Phoenix knowledge to building production GraphQL APIs, solidifying architectural thinking with a concrete, modern use case.
Discussion
Keep reading
Paths that share books, cover the same subject, or open a related topic.