By Jeff Hajewski

The Art of Elixir

A comprehensive guide to building scalable, concurrent, and fault-tolerant systems with Elixir and the BEAM VM.

From basic syntax to distributed systems, OTP patterns, and production testing — everything you need to write professional Elixir.

19 chapters 13,000+ lines of content 12 example projects
The Art of Elixir book cover

Elixir powers systems at

Discord Pinterest Pepsi Toyota Mozilla Heroku

Why Learn Elixir?

Elixir gives you superpowers that most languages can't match.

Massive Concurrency

Run tens of thousands of lightweight processes on a single machine. Elixir's actor model makes concurrency simple and safe.

🛡

Fault Tolerance

Built on the battle-tested BEAM VM that has powered telecom systems for decades. Your systems self-heal with supervisor trees.

📈

Scales Effortlessly

From a single node to a distributed cluster — Elixir's architecture means you scale without rewriting your application.

Clean & Readable

Ruby-inspired syntax with the power of functional programming. No type Tetris — just expressive, maintainable code.

What You'll Learn

A progressive journey from first steps to production-ready distributed systems.

01

Language Fundamentals

Data types, pattern matching, functions, modules, structs, and control flow — the complete foundation.

02

Functional Programming

Immutability, pure functions, Enum, Stream, and the pipe operator. Write elegant, composable code.

03

Concurrency & Processes

Lightweight processes, message passing, and the actor model. Harness multi-core processors with confidence.

04

OTP & Supervision

GenServer, Supervisors, and Applications — the battle-tested framework for building reliable systems.

05

Distributed Systems

Build systems that span multiple nodes. Inter-service communication, clustering, and real-world architectures.

06

Testing & Best Practices

ExUnit, Mox, property-based testing with StreamData, and idiomatic Elixir patterns for production code.

Inside the Book

19 chapters that take you from zero to building distributed, fault-tolerant systems. Click a chapter to preview.

01 Why Elixir?

Elixir is a dynamic, functional language designed for building scalable and maintainable applications. It runs on top of the BEAM VM, the same VM that powers Erlang, which is known for being incredibly fault-tolerant, fast, and supporting a huge level of concurrency.

Elixir's out-of-the-box support for concurrency, fault-tolerance, and functional programming is one of the main draws to the language. If you are looking for a language to build large, highly concurrent, and performant systems, Elixir should be at the top of your list. Companies like Discord, Pinterest, Pepsi, Moz, and Toyota use Elixir to power some of their highest traffic/highest demand systems.

Part of Elixir's power comes from its concurrency model. Elixir provides lightweight processes that get executed concurrently. These processes help developers to better leverage modern multi-core processors without worrying about many of the challenges of traditional thread-based concurrency. Processes are isolated and only communicate with each other through message passing. This eliminates common concurrency issues like race conditions and deadlocks.

Another major selling point for Elixir is that it is a functional language. This means functions are first class citizens and data is immutable by default. They can be assigned to variables, passed to other functions, and in general treated like data in many respects. Functions are generally pure, meaning they don't have side effects.

Many proponents of functional programming claim it leads to predictable, simpler to reason about, and maintainable code. In my experience, this is true for languages like Elixir. Elixir takes all the best parts of functional programming without requiring you to spend hours playing type Tetris like you do in Haskell or highly-functional Scala codebases.

02 Setting Up Your Environment

Get Elixir installed and configured on your machine. This chapter walks you through setting up your development environment, your first IEx session, and creating your first Mix project.

We'll cover installing Elixir via asdf, configuring your editor for Elixir development, understanding the Mix build tool, and running your first Elixir scripts. By the end you'll have a fully functional Elixir development environment ready to go.

03 Basic Syntax and Data Types

This chapter will give you just about everything you need to know to start writing some basic Elixir code. By the end of the chapter you will know how to do everything from iteration, branching, comments, basic pattern matching, and defining functions.

Elixir is a dynamic, functional programming language. This means it does not have static type checking like languages such as C++, Go, or Rust. Instead, the type of a variable is bound at runtime. We'll cover integers, floats, atoms, strings, booleans, and the special nil value, along with all the operators and conventions you need to work with them.

04 Collections

Dive into Elixir's collection types: lists, tuples, keyword lists, and maps. Understand how each is implemented, when to use which, and the performance trade-offs between them.

Lists in Elixir are implemented as singly-linked lists, making prepending efficient but random access slow. Tuples store elements contiguously in memory, offering fast access but expensive updates. Maps provide the key-value abstraction you'll use most often, with special syntax for atom keys. We'll explore all of these with practical examples and performance considerations.

05 Modules and Structs

Modules are the primary way to organize code in Elixir. Structs build on maps to give you named, compile-time-checked data structures. Together they form the backbone of every Elixir application.

Learn about module attributes, documentation with @doc and @moduledoc, using behaviours to define contracts, implementing protocols for polymorphism, and organizing your code into a clean module hierarchy. We'll also cover structs in depth, including default values, enforcing keys, and pattern matching on struct types.

06 Functions

As a functional language, functions sit at the core of Elixir. Functions can be passed as if they were data. They can be defined inline and assigned to variables. Functions may have side-effects, or they may be pure, having no side-effect at all.

In this chapter we will learn how to use functions effectively. Starting with the basics and moving on to more advanced concepts. We'll cover anonymous functions, named functions, multi-clause functions, guard clauses, default arguments, the capture operator, and how to compose functions using the pipe operator for clean, readable data transformations.

07 Flow Control

Elixir provides several constructs for controlling program flow: case, cond, if/else, and the powerful with expression. Each has its place, and knowing when to use which will make your code cleaner.

We'll explore each control structure with practical examples, showing how pattern matching integrates with case expressions, how guard clauses add precision, and how the with expression elegantly handles chains of operations that might fail. You'll also learn about exception handling with try/rescue/catch and when to use it versus pattern matching on error tuples.

08 Pattern Matching

When most people are first introduced to functional programming there are two immediate observations: the learning curve for working with immutable data is steep and pattern matching is awesome. Pattern matching is one of those language features that is hard to live without.

In Elixir, the = operator is actually called the match operator, not an assignment operator. When you write an expression like x = 42, Elixir tries to match the left-hand side with the right-hand side.

We'll go deep into destructuring tuples, lists, maps, and structs. You'll learn the pin operator for matching against existing values, how to use pattern matching in function heads for elegant multi-clause functions, and advanced techniques like matching on binary data. By the end, pattern matching will feel like a superpower you can't imagine coding without.

09 Enum and Stream

The Enum and Stream modules are workhorses of Elixir. Enum provides eager operations on collections — map, filter, reduce, and dozens more. Stream provides their lazy counterparts for working with large or infinite data sets.

Learn to compose transformations with the pipe operator, understand when lazy evaluation saves memory and when eager evaluation is faster, and master the most important functions in both modules. We'll also cover Stream.resource for building custom streams from external data sources like files, APIs, and databases.

10 Processes and Concurrency

Processes are the fundamental building blocks for concurrent programming in Elixir and form the foundation for building scalable and fault-tolerant systems. Elixir processes are lightweight and isolated. A single machine can run tens of thousands of processes without issue.

Each process has its own memory space and communicates with other processes through message passing, ensuring no shared mutable state. Elixir processes are more efficient than OS threads in terms of memory usage and context switching. We cover spawning processes, sending and receiving messages, process linking and monitoring, and the Task module for structured concurrency patterns.

11 OTP

OTP (Open Telecom Platform) is a set of libraries and design principles that come bundled with Erlang and Elixir. It provides battle-tested abstractions for building concurrent, fault-tolerant applications.

This chapter introduces OTP behaviours, the philosophy of "let it crash," and how OTP's design principles have been refined over decades of building systems that simply cannot go down. You'll understand why OTP is considered Elixir's killer feature and how it changes the way you think about building software.

12 GenServer

The GenServer behaviour directs us in how to build a server in a manner that aligns with OTP. GenServer helps us abstract the complexities of process management and message-passing by providing a high-level interface for managing state, handling requests, and ensuring robustness.

The GenServer behaviour provides a standard server interface consisting of state initialization and functions for responding to client requests. We cover handle_call, handle_cast, handle_info, init, and terminate callbacks. You'll build real GenServers including a ring buffer, a cache, and a stateful worker, learning patterns for testing and debugging them along the way.

13 Supervisor

Supervisors monitor child processes and restart them when they fail. This is the core of Elixir's "let it crash" philosophy — instead of defensive programming, you build systems that recover automatically.

Learn the different restart strategies (one_for_one, one_for_all, rest_for_one), how to structure supervision trees, dynamic supervisors for on-demand process creation, and how to design your application's process hierarchy for maximum resilience. We'll build supervision trees from scratch and visualize how failures propagate and recover.

14 Application

An Application in Elixir is more than just "your program." It's an OTP concept — a component with a defined start/stop lifecycle that can be started, stopped, and managed as a unit.

This chapter covers the Application behaviour, configuration management, environment variables, starting your supervision tree from an Application module, and how applications compose together as dependencies. You'll understand how Mix projects become running OTP applications and how to configure them for different environments.

15 Distributed Elixir

Many modern applications have some degree of real-time interaction. Building these kinds of systems involves overcoming a unique set of challenges, particularly as the load increases and you need to scale out the machines running your software.

As these systems grow, they must balance their ability to scale with their ability to fail gracefully. Maintaining data consistency and system reliability become increasingly difficult. This chapter covers node clustering, distributed process communication, inter-service communication with Apache Thrift and gRPC, umbrella applications, Broadway for data pipelines, and real-world distributed architecture patterns.

16 Testing

Testing is an essential part of any software project. One of the great features of Elixir is immutability — it makes your code simpler to test. You don't have to worry about state mutation between tests, and pure functions are straightforward to verify.

We cover ExUnit in depth, including setup and teardown, test organization, async testing, and doctests. Then we move into Mox for mocking external dependencies, and StreamData for property-based testing. You'll learn patterns for testing GenServers, supervised processes, and concurrent code — the scenarios where bugs hide in other languages.

17 Best Practices and Idioms

Write idiomatic Elixir that other developers will thank you for. This chapter covers naming conventions, the pipe operator, pattern matching style, and the small decisions that separate good Elixir from great Elixir.

Pragmatic guidance showing "good" vs. "better" approaches to common patterns. Covers when to use with vs. case, how to structure module APIs, when to reach for protocols vs. behaviours, and conventions around error handling, naming, and code organization that the Elixir community has settled on.

A Appendix: BEAM Internals

Go under the hood of the virtual machine that makes it all possible. Understand how the BEAM schedules processes, manages memory, and achieves the performance characteristics that set Elixir apart.

This appendix covers the BEAM scheduler, process memory layout, garbage collection strategies including generational and per-process GC, the reduction counting model, and how the BEAM achieves soft real-time guarantees. Essential reading for anyone who wants to understand why Elixir performs the way it does.

Code That Speaks for Itself

Elixir is expressive, readable, and powerful. Here's a glimpse.

Pattern Matching
defmodule Greeter do
  def hello("Elixir"), do: "You're speaking my language!"
  def hello(name), do: "Hello, #{name}!"
end

Greeter.hello("Elixir")  # "You're speaking my language!"
Greeter.hello("World")   # "Hello, World!"
Concurrency
# Spawn thousands of lightweight processes
1..10_000
|> Enum.each(fn i ->
  spawn(fn -> IO.puts("Process #{i} running") end)
end)

# Each process is isolated, concurrent,
# and costs only ~2KB of memory

Who This Book Is For

Developers New to Elixir

Whether you come from Ruby, Python, JavaScript, or Java — this book gives you a clear, progressive path into Elixir and functional programming.

Backend Engineers

Building APIs, real-time systems, or data pipelines? Learn why companies like Discord chose Elixir for their most demanding workloads.

System Architects

Understand OTP supervision trees, distributed clustering, and fault-tolerant design patterns you won't find in typical language guides.

About the Author

Jeff Hajewski is a software engineer with deep experience in building scalable systems. His pragmatic, hands-on teaching style emphasizes real-world patterns and idiomatic code over academic theory.

The Art of Elixir distills years of production experience into a book that takes you from your first iex session to deploying distributed, fault-tolerant applications.

Get The Art of Elixir

Start building scalable, fault-tolerant systems today.

The Art of Elixir

The Art of Elixir

By Jeff Hajewski · Remington Shaw Publishing

  • 19 in-depth chapters
  • 12 working example projects
  • From fundamentals to distributed systems
  • BEAM VM internals appendix
  • Best practices and idiomatic patterns

ISBN: 979-8-218-62881-9

Buy on Amazon