Top 42 Elixir Interview Questions & Answers (2026)
About Elixir
Top 50 Elixir interview questions covering functional programming, OTP, GenServer, Phoenix, concurrency, fault tolerance, and the BEAM VM. Companies hiring for Elixir roles test this knowledge at every stage — from first-round screens that check the basics to final-round conversations about real-world trade-offs.
What to Expect in a Elixir Interview
Expect a mix of conceptual and practical Elixir questions: clear definitions and core-concept checks for junior roles, hands-on scenario questions for mid-level roles, and architecture or trade-off discussions for senior roles. Interviewers usually move from foundational topics toward the kind of problems you'd actually face on the job.
How to Use This Guide
Work through the Elixir questions in order — Beginner, then Intermediate, then Advanced — so each concept builds on the last. Every question has its own page; bookmark the ones that trip you up and revisit them the day before your interview.
Curated by Tech Baithak Editorial Team · Last updated: June 2026
Core concepts every Elixir developer must know.
01
What is Elixir?
Elixir is a dynamic, functional programming language designed for building scalable and maintainable applications. Created by José Valim (a former Ruby on Rails core team member), it runs on the BEAM virtual machine (the Erlang VM), inheriting Erlang's decades of battle-tested reliability in distributed and concurrent systems. Key characteristics: Functional: immutable data, first-class functions, pattern matching. Concurrent: lightweight processes (not OS threads), message passing. Fault-tolerant: supervisor trees, "let it crash" philosophy. Distributed: built-in support for distributed nodes. Scalable: millions of lightweight processes per node. Elixir is used for web applications (Phoenix framework), IoT (Nerves), real-time systems, and any domain requiring high availability. Its syntax is inspired by Ruby, making it approachable for Ruby developers.
02
What is the BEAM virtual machine?
The BEAM (Bogdan's Erlang Abstract Machine, sometimes called the Erlang VM) is the virtual machine that runs Elixir and Erlang programs. It is fundamentally different from the JVM — designed specifically for concurrent, distributed, fault-tolerant systems. Key properties: Lightweight processes: BEAM processes are not OS threads — they are extremely lightweight (2KB heap by default), and millions can run concurrently on a single node. Preemptive scheduling: the BEAM scheduler ensures no single process can starve others, unlike cooperative multitasking. Per-process garbage collection: each process has its own heap and GC cycle — a GC pause in one process doesn't stop others. Hot code swapping: code can be upgraded in a running system without downtime. Distribution: BEAM nodes can communicate transparently across a network. These properties make Elixir/Erlang uniquely suited for systems requiring 99.999% uptime (like telecom infrastructure).
03
What is pattern matching in Elixir?
Pattern matching is one of Elixir's most important features. The = operator in Elixir is not assignment — it is a match operator. It attempts to match the right side against the left side pattern. x = 1 binds x to 1. {a, b} = {1, 2} binds a=1 and b=2. [head | tail] = [1, 2, 3] binds head=1, tail=[2,3]. Failed match raises MatchError. Use in function clauses: def handle({:ok, value}), do: process(value); def handle({:error, reason}), do: log(reason). Pattern matching in case: case response do {:ok, data} -> data; {:error, _} -> nil end. Pin operator ^: use existing value instead of rebinding: ^x = 1 — matches only if the value equals the current x. Pattern matching makes Elixir code extraordinarily expressive and eliminates most conditional logic.
04
What are Elixir processes?
Elixir processes are the basic unit of concurrency — extremely lightweight, isolated actors running on the BEAM. They are not OS threads or OS processes. Key properties: Lightweight: start with ~2KB heap, grow as needed. Millions can run concurrently. Isolated: each process has its own memory heap. No shared mutable state. Communication: processes communicate only by sending immutable messages. Independent GC: each process is garbage collected independently — no global GC pauses. Create a process: pid = spawn(fn -> IO.puts("Hello") end). Send a message: send(pid, {:hello, "world"}). Receive: receive do {:hello, msg} -> IO.puts(msg) end. Processes can be linked (failure propagates) or monitored (failure sends a message). This model makes building concurrent systems composable and predictable compared to shared-memory threading.
05
What is OTP in Elixir?
OTP (Open Telecom Platform) is a set of libraries, behaviors, and design principles for building fault-tolerant, concurrent systems in Erlang/Elixir. Despite the name, it is not specific to telecom — it is the standard framework for production Elixir/Erlang systems. Key OTP components: GenServer: a generic server behavior for stateful processes. Supervisor: monitors child processes and restarts them according to a strategy when they crash. Application: packages a set of processes into a deployable unit. Agent: simple stateful process (thin wrapper over GenServer). Task: run a single computation asynchronously. Registry: process name registration. OTP transforms the "let it crash" philosophy into a structured supervision tree — instead of defensive error handling, processes crash cleanly and supervisors restart them. Systems built with OTP can achieve extremely high uptime (Erlang/OTP systems have legendary 9-nines availability records).
06
What is GenServer in Elixir?
GenServer (Generic Server) is an OTP behavior for implementing stateful server processes. It abstracts the common patterns of process lifecycle, state management, and message handling. Define a GenServer: defmodule Counter do; use GenServer; def start_link(initial), do: GenServer.start_link(__MODULE__, initial); def init(state), do: {:ok, state}; def handle_call(:get, _from, state), do: {:reply, state, state}; def handle_cast(:increment, state), do: {:noreply, state + 1}; end. handle_call: synchronous request-response. handle_cast: asynchronous fire-and-forget. handle_info: handle arbitrary messages (e.g., timeouts, exits). Client API: GenServer.call(pid, :get), GenServer.cast(pid, :increment). GenServers are the workhorse of Elixir applications — used for caches, connection pools, background workers, state machines, and any stateful component that needs lifecycle management and fault tolerance.
07
What is a Supervisor in Elixir?
A Supervisor is an OTP process that monitors child processes and restarts them according to a restart strategy when they crash. This embodies the "let it crash" philosophy — instead of defensive error handling, let processes fail and rely on supervisors to recover them. Define a Supervisor: defmodule MyApp.Supervisor do; use Supervisor; def start_link(_), do: Supervisor.start_link(__MODULE__, []); def init([]), do: Supervisor.init([{MyWorker, []}, {MyCache, []}], strategy: :one_for_one) end. Restart strategies: :one_for_one: restart only the crashed child. :one_for_all: restart all children when one crashes. :rest_for_one: restart the crashed child and all children started after it. Restart values: :permanent (always restart), :temporary (never restart), :transient (restart only on abnormal exit). Supervisors form a tree — an entire application is a hierarchy of supervisors and workers.
08
What are Elixir modules and functions?
In Elixir, modules are the primary namespace and code organization unit. Define with defmodule: defmodule Greeter do; def greet(name), do: "Hello, #{name}"; defp private_helper, do: "private"; end. def: public function. defp: private function (only accessible within the module). Functions are identified by name and arity: greet/1. Multiple clauses with pattern matching: def process({:ok, v}), do: v; def process({:error, e}), do: raise e. Guards: additional constraints: def abs(n) when n < 0, do: -n; def abs(n), do: n. Function capture: &Greeter.greet/1 or &(&1 * 2). Modules can also define attributes (@moduledoc, @doc, custom @attribute value), macros (defmacro), structs, and implement behaviours. Modules are compiled to BEAM bytecode.
09
What are Elixir data types?
Elixir's core data types: Integers and Floats: 42, 3.14. Atoms: named constants — :ok, :error, true, false, nil. Module names are atoms. Strings: UTF-8 binaries — "hello". Strings support interpolation: "Hello, #{name}". Charlists: lists of code points — 'hello' (single quotes — legacy, mostly for Erlang interop). Lists: linked lists — [1, 2, 3]. Prepend O(1): [0 | list]. Tuples: fixed-size collections — {:ok, 42}. Random access O(1). Maps: key-value stores — %{name: "Alice", age: 30}. Structs: named maps with defined keys — %User{name: "Alice"}. Binaries: raw byte sequences — <<1, 2, 3>>. Functions: first-class values. All Elixir data is immutable — operations return new values.
10
What is the pipe operator in Elixir?
The pipe operator (|>) passes the result of the left expression as the first argument to the right function. It enables readable, left-to-right function composition without deeply nested calls. Without pipe: String.downcase(String.trim(String.replace(input, " ", "_"))). With pipe: input |> String.replace(" ", "_") |> String.trim() |> String.downcase(). The result of each step is the first argument of the next. It is similar to Unix pipes: cat file | grep pattern | sort | uniq. Real example: users |> Enum.filter(&(&1.active)) |> Enum.sort_by(&(&1.name)) |> Enum.take(10). The pipe operator encourages data-first function design — functions should take the primary data as the first argument. It is one of the most beloved Elixir features for producing readable data transformation pipelines.
11
What is Phoenix Framework?
Phoenix is the web framework for Elixir, analogous to Rails for Ruby or Laravel for PHP. It is known for exceptional performance — handling millions of WebSocket connections on a single server. Key components: Router: maps HTTP requests to controller actions. Controllers: handle requests, call contexts, return responses. Views/Templates: render HTML with HEEx templates. Contexts: bounded domain logic modules (like service layers). Channels: real-time WebSocket communication — each connected user is a lightweight BEAM process. LiveView: server-rendered, real-time interactive UIs — HTML diffs are sent over WebSocket, eliminating most JavaScript. Ecto: database library with changesets for validation. Phoenix leverages BEAM's concurrency — unlike Node.js or Rails, it handles long-lived connections natively without blocking. It is used by Fly.io, Discord (millions of concurrent users), Bleacher Report, and many others.
12
What is Ecto in Elixir?
Ecto is Elixir's database library — it is not just an ORM but a comprehensive toolkit for working with data. Key components: Repo: the database interface. Repo.insert(changeset), Repo.all(query), Repo.get(Model, id). Schema: defines the data model and its database mapping: schema "users" do field :name, :string; field :age, :integer; timestamps() end. Changeset: validates and transforms data before insertion/update. def changeset(user, attrs) do user |> cast(attrs, [:name, :age]) |> validate_required([:name]) |> validate_number(:age, greater_than: 0) end. Query DSL: composable, safe queries: from u in User, where: u.age > 18, select: u. Migrations: version-controlled schema changes. Ecto separates the database concern (Schema) from validation (Changeset) — a design that prevents common bugs where validation is bypassed by going directly to the database.
13
What is Phoenix LiveView?
Phoenix LiveView enables rich, real-time user interfaces without writing JavaScript. A LiveView component renders HTML on the server and maintains a persistent WebSocket connection with the browser. When server state changes, LiveView computes a minimal HTML diff and sends it to the browser — which efficiently patches the DOM. Key features: Server-side rendering: initial page load is fast (full HTML). Real-time updates: push updates to the client over WebSocket. Event handling: phx-click, phx-change, phx-submit attributes send DOM events to the server. LiveComponents: stateful, isolated UI components within a LiveView. Streams: efficiently manage large lists without sending all data. Example use cases: forms with live validation, real-time dashboards, search-as-you-type, collaborative tools. LiveView eliminates the need for a separate JavaScript SPA in many cases, reducing complexity and keeping all logic on the server in Elixir.
14
What is immutability in Elixir?
In Elixir, all data is immutable — once a value is created, it cannot be modified. Operations on data return new values. For example: list = [1, 2, 3]; new_list = [0 | list] — list is still [1, 2, 3]; new_list is [0, 1, 2, 3]. Map update: map = %{a: 1}; new_map = %{map | a: 2} — map is unchanged. Immutability enables: Concurrency safety: no shared mutable state means no race conditions, no need for locks. Reasoning about code: a function's output depends only on its inputs — no hidden state changes. Efficient memory: the BEAM shares immutable data between processes structurally — no copying unless needed. Variable rebinding (x = x + 1) creates a new binding, not mutation. This is why Elixir code is inherently thread-safe without synchronization primitives for most patterns.
15
What are Elixir structs?
Structs in Elixir are typed maps with compile-time key checking and default values. Define inside a module: defmodule User do; defstruct name: nil, age: 0, active: true; end. Create: user = %User{name: "Alice", age: 30}. Access: user.name. Update: %{user | age: 31}. Unlike plain maps, structs have a defined set of keys — accessing an undefined key is a compile-time error. Pattern match: %User{name: name} = user. Structs have a __struct__ field that contains the module name, enabling pattern matching by type: %User{} = user. They can implement Elixir protocols (like String.Chars, Inspect). Structs are used to represent domain entities, configuration, and any typed data. They are essentially named maps — the underlying data is still a map, but with validation and better introspection.
16
What is the "let it crash" philosophy in Elixir?
The "let it crash" philosophy is a fundamental Elixir/Erlang design principle: instead of writing defensive code to handle every possible error, allow processes to fail and rely on supervisors to restart them in a clean state. In most languages, errors must be caught and handled at every level to avoid crashing the entire application. In Elixir, each process is isolated — a crash in one process does not affect others. Supervisors monitor processes and restart them according to a strategy. Benefits: Simpler code: happy path only, no defensive checks for unlikely errors. Correct recovery: a fresh process starts with known-good initial state — better than continuing in a potentially corrupted state. Fault isolation: failures are contained and recovered automatically. Real-world example: if a database connection process crashes, the supervisor restarts it with a fresh connection — users of the system may see a brief error but the system self-heals. This philosophy has produced telecom systems with 99.999% uptime records.
17
What is ExUnit in Elixir?
ExUnit is Elixir's built-in testing framework. Tests are organized in test modules: defmodule MyModuleTest do; use ExUnit.Case; test "adds numbers" do; assert 1 + 1 == 2; end; end. Key features: assert/refute: intelligent assertions with descriptive failure messages that show the actual and expected values. doctest: test code examples in documentation: doctest MyModule. setup/setup_all: test fixtures — run before each test or once per module. async: true: run test modules concurrently for faster test suites. Mocking: Elixir community prefers dependency injection over mocking — pass modules as arguments. Libraries like Mox provide mock-friendly behaviour-based mocking. Run tests: mix test. Tags and filters: @tag :slow; mix test --exclude slow. ExUnit's clarity and speed make Elixir development test-friendly from the start.
18
What is Mix in Elixir?
Mix is Elixir's build tool and project management system — the equivalent of npm/cargo/mix (or rake in Ruby). Key commands: mix new my_app: create a new project. mix compile: compile the project. mix test: run tests. mix deps.get: fetch dependencies (like npm install). mix run: run a script. mix phx.server: start a Phoenix server. mix ecto.migrate: run database migrations. Mix uses mix.exs for project configuration and dependencies (similar to package.json). Dependencies are fetched from Hex (Elixir's package registry, hex.pm). Custom mix tasks: defmodule Mix.Tasks.MyTask do; use Mix.Task; def run(_), do: IO.puts("hello"); end. Mix integrates with the broader OTP tooling and makes Elixir project management straightforward.
19
What is the difference between send/receive and GenServer in Elixir?
send/receive is the raw, low-level mechanism for inter-process communication. Any process can send any message to any PID: send(pid, {:hello, "world"}); receive do {:hello, msg} -> msg end. You manually handle the message loop, timeouts, and unexpected messages. GenServer is an OTP behaviour built on top of send/receive that provides: a standardized message protocol, automatic handling of timeouts and unexpected messages (via handle_info), synchronous calls (call) vs async casts (cast), process registration and supervision integration, tracing and debugging support, and graceful shutdown via terminate callback. When to use send/receive: short-lived tasks (via Task), fire-and-forget scenarios, or learning/experimentation. When to use GenServer: any production stateful process — GenServer provides the boilerplate and guarantees that raw messaging lacks. Most Elixir production code uses GenServer over manual message passing.
20
What are Elixir protocols?
Protocols in Elixir enable polymorphism for any data type, including types you don't own. Define a protocol: defprotocol Printable do; def print(data); end. Implement for specific types: defimpl Printable, for: Integer do; def print(n), do: IO.puts(n); end; defimpl Printable, for: List do; def print(l), do: IO.inspect(l); end. Call: Printable.print(42); Printable.print([1,2,3]). Key feature: you can implement a protocol for a type you don't control — a third-party library's struct, or built-in types. This is different from Ruby's monkey patching because protocol implementations are explicit and scoped. Elixir's standard library uses protocols extensively: Enumerable (enables Enum functions on any custom type), String.Chars (enables string interpolation), Inspect (enables IO.inspect). Protocols are the Elixir equivalent of type classes in Haskell/Scala.
Practical knowledge for developers with hands-on experience.
01
How do Elixir supervisors implement fault tolerance?
Supervisors implement fault tolerance through the supervision tree: a hierarchy where each supervisor monitors its children and applies restart strategies. The key mechanism: Process linking: spawn_link links parent and child — if the child crashes, the parent receives an exit signal. Supervisors trap exits (Process.flag(:trap_exit, true)) and handle them. Restart strategies: :one_for_one (only the crashed process restarts), :one_for_all (all children restart — for tightly coupled processes), :rest_for_one (crashed process and later-started siblings restart). Restart intensity limits: max restarts within a time window — if exceeded, the supervisor itself crashes (propagating up the tree). Dynamic supervisors (DynamicSupervisor): start children at runtime (e.g., one process per connected user). The entire application is a tree of supervisors — a crash anywhere causes a localized recovery rather than system-wide failure. This architecture is why Erlang/Elixir systems can achieve "nine nines" uptime.
02
What is Phoenix Channels and how do WebSockets work in Elixir?
Phoenix Channels provide real-time, bidirectional communication over WebSockets (or long polling as fallback). Each connected client gets a dedicated BEAM process (lightweight, ~2KB) — a server handling 2 million concurrent connections uses only ~4GB RAM for the connection processes. Channel structure: Socket: the WebSocket connection handler. Channel: a topic-based room within a socket: defmodule MyAppWeb.RoomChannel do; use Phoenix.Channel; def join("room:" <> _id, _params, socket), do: {:ok, socket}; def handle_in("message", %{"body" => body}, socket) do; broadcast!(socket, "message", %{body: body}); {:noreply, socket}; end; end. Client connects: socket.channel("room:123").push("message", {body: "hello"}). Presence: built-in distributed user tracking — know who is online across multiple nodes. Discord used Phoenix Channels to serve 5 million concurrent WebSocket connections on a single server before needing to scale horizontally.
03
What is Elixir's concurrency model compared to threads?
Elixir/BEAM processes are fundamentally different from OS threads. OS threads: heavy (~1-2MB stack), managed by the OS kernel, share memory, require synchronization primitives (mutexes, semaphores), and context switching is expensive. Typical limit: thousands per machine. BEAM processes: extremely lightweight (~2KB initial heap), managed by BEAM schedulers (not the OS), share no memory (message passing only), require no synchronization, context switching is fast and cheap. Typical capability: millions per node. Scheduling: BEAM uses preemptive reduction-based scheduling — each process gets a fixed number of "reductions" (function calls) before yielding, ensuring fair scheduling. No process can monopolize the scheduler. Concurrency vs Parallelism: BEAM schedulers run one per CPU core, achieving true parallelism on multi-core machines. Memory isolation: each process has a private heap and GC — a GC cycle in one process never stops others. This model makes writing concurrent Elixir code dramatically safer than thread-based languages.
04
What are Elixir behaviours?
Behaviours define a set of functions (callbacks) that a module must implement — they are Elixir's way to specify a contract or interface. Define a behaviour: defmodule Parser do; @callback parse(String.t()) :: {:ok, any()} | {:error, String.t()}; end. Implement it: defmodule JsonParser do; @behaviour Parser; def parse(s) do ... end; end. The compiler warns if a required callback is missing. Common OTP behaviours: GenServer, Supervisor, Application, GenEvent. Difference from protocols: behaviours define what a module must do (module-level contract); protocols enable dispatch based on data type (value-level polymorphism). Use behaviours for: pluggable implementations (payment gateways, notification providers), testing (swap real implementation with a mock module), and defining expected interfaces for adapter patterns. Behaviours are documented via @callback annotations and type specs.
05
What is Elixir's error handling approach?
Elixir distinguishes between expected errors and unexpected failures. Expected errors: use tagged tuples — functions return {:ok, result} or {:error, reason}. Handle with pattern matching: case File.read(path) do {:ok, content} -> process(content); {:error, reason} -> log(reason) end. With: chain multiple ok/error operations: with {:ok, user} <- find_user(id), {:ok, order} <- find_order(user), do: process(order) — short-circuits on first error. Unexpected errors: let the process crash (supervisor restarts it). Avoid excessive defensive coding. try/rescue: for exceptional cases requiring exception handling (e.g., wrapping Erlang code): try do risky_operation() rescue e in RuntimeError -> handle(e) end. raise/throw: for programmer errors and control flow within a process. The ! convention: File.read! raises on error vs File.read returning {:ok, _}. Most Elixir code uses tagged tuples over exceptions for expected failure modes.
06
What is a GenStateMachine in Elixir?
GenStateMachine (or :gen_statem in Erlang) is an OTP behaviour for implementing finite state machines as processes. It models a process as a set of states, events, and transitions between states. Each state has a handler function that processes events and returns the next state. Example states for a traffic light: :red, :green, :yellow. Define transitions: def red(:cast, :next, data), do: {:next_state, :green, data}. This is cleaner than a GenServer with a state field because the state machine logic is explicit in the state handler functions. Use cases: network protocol implementations (TCP connection states), order workflows (pending → confirmed → shipped → delivered), authentication flows, connection lifecycle management. The BEAM has been running state machines this way in telecom switches since the 1980s — protocols like SCTP are implemented as gen_statem in Erlang.
07
How does hot code upgrading work in Elixir/Erlang?
Hot code upgrading allows updating a running BEAM system without stopping it — an Erlang/Elixir unique capability. The BEAM can load new module versions while old code continues running: two versions exist simultaneously during the transition. Releases: Elixir uses mix release to build deployable packages. Hot upgrades use appups and relups (release upgrade scripts) that describe how to transform state from the old to new version. Process code switching: running GenServers automatically switch to the new module version on their next function call. code_change callback: GenServers implement code_change to transform their state when upgraded. Practical use: while hot upgrades are powerful (Erlang telecom systems use them for zero-downtime upgrades), most Phoenix/Elixir applications use rolling deployments with fast restart instead — it is simpler and sufficient for most applications. True hot upgrades are used in embedded systems (Nerves) and telecom where any downtime is unacceptable.
08
What is ETS (Erlang Term Storage) in Elixir?
ETS (Erlang Term Storage) is an in-memory key-value store built into the BEAM, accessible from any process with no copying overhead (data is shared via references). Create a table: :ets.new(:my_cache, [:named_table, :public, read_concurrency: true]). Insert: :ets.insert(:my_cache, {:user_1, %{name: "Alice"}}). Lookup: :ets.lookup(:my_cache, :user_1). Types: :set (default, unique keys), :bag (multiple values per key), :ordered_set. Access types: :public (any process), :protected (only owner writes), :private. ETS is significantly faster than GenServer for read-heavy shared state because reads don't serialize through a single process mailbox. Use cases: caching database query results, rate limiting counters, session storage, compiled route tables. DETS: disk-based ETS for persistence. Mnesia: distributed database built on ETS for multi-node state.
09
What is the Registry module in Elixir?
The Registry module provides a process registry backed by ETS — a way to give processes names beyond simple atoms (which require globally unique names). Unique registry: one process per key: Registry.start_link(keys: :unique, name: MyApp.Registry); {:ok, pid} = MyWorker.start_link(user_id: 42); Registry.register(MyApp.Registry, "user:42", %{}); Registry.lookup(MyApp.Registry, "user:42"). Duplicate registry: multiple processes per key (like pub-sub). Use in GenServer: register via via_tuple: def start_link(name), do: GenServer.start_link(__MODULE__, [], name: {:via, Registry, {MyApp.Registry, name}}). Then address by name: GenServer.call({:via, Registry, {MyApp.Registry, name}}, :ping). Registry is essential for per-entity processes — one process per user session, per device, per chat room. It scales to millions of entries and handles process death/deregistration automatically.
10
What is Task in Elixir?
Task is an OTP abstraction for one-shot asynchronous computations. Unlike GenServer (long-lived stateful processes), Tasks are for running a single computation and getting the result. async/await: task = Task.async(fn -> slow_computation() end); result = Task.await(task, 5000) — starts computation in a new process, blocks until result with timeout. start/async_stream: fire-and-forget computation. Task.async_stream: process a list concurrently with bounded parallelism: results = 1..100 |> Task.async_stream(&fetch_item/1, max_concurrency: 10) |> Enum.to_list(). Task.Supervisor: supervise dynamically started tasks — handle failures and restart policies. Tasks are supervised by the calling process by default (linked). Use Task.Supervisor.async_nolink for independent tasks. Tasks are perfect for parallel API calls, background computations, and data pipeline stages where you want structured concurrency without a full GenServer.
11
What is Elixir's Enum and Stream modules?
Enum and Stream are Elixir's collection processing modules. Enum: eager evaluation — processes all elements immediately and returns a concrete collection. Enum.map([1,2,3], &(&1 * 2)), Enum.filter, Enum.reduce, Enum.sort, Enum.group_by, Enum.chunk_every. Works with any type implementing the Enumerable protocol. Stream: lazy evaluation — creates a computation pipeline without executing it until needed. Stream.map, Stream.filter, Stream.flat_map. Compose: stream = 1..1_000_000 |> Stream.filter(&(rem(&1, 2) == 0)) |> Stream.map(&(&1 * 2)) |> Stream.take(5); Enum.to_list(stream) — only processes elements until 5 are found, never materializing the entire filtered list. When to use Stream: large or potentially infinite data, reading large files line by line, pipeline stages where you can short-circuit. When to use Enum: small collections where lazy overhead is not worth it.
12
How does Elixir handle distributed computing?
Elixir/Erlang has built-in distribution via the BEAM's distributed messaging layer. Connect nodes: Node.connect(:"node2@host"). Send to a remote process: send({:name, :"node2@host"}, message). Spawn on remote node: Node.spawn(:"node2@host", fn -> ... end). Distributed GenServer calls: using :via with a distributed registry like Horde. Key features: Location transparency: send/receive and GenServer work identically across nodes. Global name registry: :global.register_name for cluster-wide unique names. Mnesia: distributed, replicated database built into Erlang. LibCluster: automatic cluster formation (Kubernetes, DNS, EC2). Horde: distributed Supervisor and Registry using delta-CRDTs. Phoenix Presence: distributed, eventually consistent user presence tracking. The BEAM's distribution model predates modern service meshes by decades and handles network partitions with clear CAP theorem trade-offs.
Deep expertise questions for senior and lead roles.
01
What is the Elixir macro system and how does metaprogramming work?
Elixir's macro system is a first-class feature — the compiler itself is written in Elixir macros. Macros operate on the AST (Abstract Syntax Tree) at compile time, transforming code before it is compiled to BEAM bytecode. Define a macro: defmacro my_if(condition, do: body) do; quote do; case unquote(condition) do; true -> unquote(body); false -> nil; end; end; end. quote creates an AST. unquote injects values into the AST. Unlike C macros (text substitution), Elixir macros are hygenic and work with the full language. Use macros sparingly: they are powerful but increase complexity. Most Elixir code that looks like magic (use GenServer, defmodule, Ecto schema DSL, Phoenix router) is implemented as macros. The use macro: use ModuleName calls ModuleName.__using__/1 which typically injects code via quote/defmacro. Metaprogramming enables creating DSLs (domain-specific languages) embedded within Elixir.
02
What is Elixir's GenServer deep internals and message queue management?
Understanding GenServer internals reveals how to build reliable production systems. Mailbox: each process has a message mailbox (queue). GenServer's receive loop processes messages in FIFO order. Selective receive: handle_info receives non-call/cast messages — timer expiries, monitor notifications, linked process exits. Hibernation: {:noreply, state, :hibernate} forces GC and minimal memory mode — ideal for long-lived processes with infrequent messages. Timeout: {:noreply, state, 5000} sends a :timeout message if no message arrives within 5 seconds. Continuing: {:continue, action} schedules handle_continue immediately after the current callback — for breaking long init work into steps without blocking the mailbox. Backpressure: GenServer mailbox can fill up under load — use Process.info(pid, :message_queue_len) to monitor. Dead letter: unexpected messages pile up in the mailbox if handle_info doesn't match — always add a catch-all clause.
03
What is Broadway in Elixir and how does it handle data pipelines?
Broadway is Elixir's concurrent, multi-stage data processing pipeline library built on top of GenStage. It provides a higher-level API for processing data from producers (SQS, Kafka, RabbitMQ, etc.) through processing stages and into batch consumers. Define a pipeline: defmodule MyPipeline do; use Broadway; def start_link(_), do: Broadway.start_link(__MODULE__, name: __MODULE__, producer: [module: {BroadwaySQS.Producer, queue_url: url}], processors: [default: [concurrency: 10]], batchers: [default: [batch_size: 100, batch_timeout: 1000]]); def handle_message(_, msg, _), do: process(msg); def handle_batch(_, msgs, _, _), do: bulk_insert(msgs); end. Features: automatic back-pressure, rate limiting, acknowledgement management, error handling with retry/discard semantics, metrics (message rate, processing time), and graceful shutdown. Broadway is ideal for processing message queues, event streams, and any data pipeline requiring controlled throughput with back-pressure.
04
What is GenStage in Elixir?
GenStage is a specification for building back-pressure-aware data pipelines in Elixir. It extends GenServer with demand-driven flow — producers only produce what consumers request. Three roles: :producer (generates data), :consumer (processes data), :producer_consumer (both — a transformation stage). Key mechanism: consumers send demand to producers; producers respond with events. handle_demand(demand, state): produce up to demand events. handle_events(events, _from, state): process received events. Back-pressure: if a consumer is slow, it simply doesn't request more events — the pressure propagates upstream automatically. Dispatch strategies: GenStage.BroadcastDispatcher (all consumers get all events), GenStage.DemandDispatcher (distribute based on demand), GenStage.PartitionDispatcher (route to specific consumers by key). GenStage is the foundation of Broadway and provides the primitives for building custom streaming data systems.
05
How does Elixir handle cluster formation and distributed state?
Cluster formation in Elixir production: LibCluster: auto-forms BEAM clusters using DNS (Kubernetes headless services), Epmd, EC2 tags, or gossip protocols. Nodes join automatically when they discover each other. State distribution strategies: (1) Horde (CRDT-based distributed supervisor and registry): processes can run anywhere in the cluster; Horde ensures a process runs on exactly one node, migrating on node join/leave. (2) Distributed Erlang messaging: route messages to any PID on any node using Node.spawn or distributed :rpc.call. (3) Local state with pub/sub: each node is authoritative for some partition of data; Phoenix PubSub broadcasts changes cluster-wide. (4) Mnesia: replicated database with RAM and disk tables, supporting transactions across nodes. Challenges: network partitions, split-brain scenarios — Elixir/BEAM does not automatically resolve these; design systems to handle partition tolerance explicitly.
06
What is Nx and Machine Learning in Elixir?
Nx (Numerical Elixir) is a tensor library for Elixir that enables numerical computing and machine learning on CPUs and GPUs. It brings NumPy/PyTorch-like capabilities to Elixir. Key features: Defn compiler: functions marked with defn are compiled to efficient numerical code (EXLA for XLA/GPU, Torchx for PyTorch backend). Automatic differentiation: Nx.Defn.grad/2 computes gradients for backpropagation. Axon: neural network library built on Nx with a Keras-like API. Bumblebee: pre-trained model serving (GPT, Whisper, Stable Diffusion) in Elixir. Explorer: DataFrame library (Pandas equivalent) using Polars backend. Use cases: serving ML models in Elixir services without Python microservices, processing data with NumPy-like operations, building ML training pipelines. Nx integrates with Phoenix LiveDashboard for real-time ML monitoring and with Livebook (Jupyter equivalent for Elixir) for interactive ML notebooks.
07
What are Elixir's process monitoring, tracing, and debugging tools?
Elixir's BEAM VM provides exceptional introspection and debugging tools. Observer: :observer.start() launches a GUI showing process list, supervision tree, ETS tables, memory usage, and message queues. :sys module: get/replace GenServer state: :sys.get_state(pid). Trace process: :sys.trace(pid, true). :dbg module: trace function calls across all processes: :dbg.tracer(); :dbg.p(pid, :all); :dbg.tpl(MyModule, :my_function, :x). Recon: production-safe process introspection library. :erlang.process_info: get process details (mailbox length, memory, reductions, backtrace). Telemetry: structured event system for metrics collection (used by Phoenix, Ecto, Broadway). AppSignal, Datadog: APM with BEAM-aware agents. Livebook: interactive notebooks for exploring running applications. Unlike most languages, BEAM's introspection works on running production systems without restarting — you can attach a console (iex --remsh) to a production node and inspect state live.
08
What is the Reactor library and event-driven architectures in Elixir?
Reactor is a dynamic, concurrent workflow engine for Elixir, enabling saga patterns and complex business workflows with automatic compensation. Define a workflow: defmodule CheckoutFlow do; use Reactor; step :charge_card, ChargeCardStep; step :create_order, CreateOrderStep, max_retries: 3; step :send_email, SendEmailStep; end. Steps can be run in parallel, with dependencies, retries, and compensation (undo actions on failure). This goes beyond Broadway's linear pipelines to arbitrary DAG (Directed Acyclic Graph) workflows. Event-driven architecture patterns in Elixir: Phoenix PubSub: in-process or cluster-wide pub-sub (built into Phoenix). EventBus: publish-subscribe with filtering and transformation. CQRS with Commanded: event sourcing framework — all state changes as immutable events, projections rebuild read models from event stream. Commanded + EventStore: full event sourcing stack. Elixir's process model makes event-driven architectures particularly natural — each aggregate root can be a process maintaining its own state.
09
How does Elixir's Ecto handle complex queries and associations?
Ecto's query DSL handles complex SQL patterns. Composable queries: build queries incrementally: query = from u in User; query = if filter_active, do: where(query, [u], u.active == true), else: query; Repo.all(query). Associations: define in schema: has_many :posts, Post; belongs_to :user, User. Load: Repo.preload(user, :posts) or with query: from u in User, preload: [:posts]. Join: from u in User, join: p in Post, on: p.user_id == u.id, select: {u, p}. Aggregates: from o in Order, group_by: o.status, select: {o.status, count(o.id)}. Fragments: raw SQL when needed: fragment("date_trunc('month', ?)", p.inserted_at). Schemaless queries: query without schema for reporting: Repo.all(from "orders", select: [:id, :total]). Multi-tenancy: prefix schemas with Repo.all(query, prefix: tenant_id). Ecto's separation of schema, changeset, and query makes complex database access patterns manageable while maintaining type safety.
10
What is the Commanded library and event sourcing in Elixir?
Commanded is an Elixir framework for building CQRS (Command Query Responsibility Segregation) and event-sourced applications. In event sourcing, state is derived by replaying an immutable sequence of events. Core concepts: Aggregate: domain object holding state, handling commands, emitting events. Command: intent to change state — defstruct [:account_id, :amount]. Event: fact that occurred — defstruct [:account_id, :amount]. Aggregate implementation: def execute(%Account{balance: balance}, %Withdraw{amount: amount}) when balance >= amount, do: %MoneyWithdrawn{amount: amount}; def apply(%Account{} = account, %MoneyWithdrawn{amount: amount}), do: %{account | balance: account.balance - amount}. Projections: Ecto-based read models updated by listening to events. Event handlers: react to events (send notifications, update caches). EventStore: persistent event log (PostgreSQL). Commanded with EventStore provides a complete event sourcing stack that leverages Elixir's immutability and concurrency strengths.