Skip to content
Back to Interview Guides
Interview Guide

25 Advanced Elixir Phoenix Interview Questions for Experienced Developers

· 14 min read
Elixir Phoenix Q&A Component

Jump to Category

⚡ Elixir Core & OTP Principles ️ Phoenix Framework Internals
Real-Time Features (Channels & LiveView) Ecto & Data Persistence
️ Architecture & Testing

Elixir Core & OTP Principles

1. Explain the different supervision strategies in OTP.

A Supervisor’s strategy determines what it does when one of its child processes crashes.

  • `:one_for_one` (default): If a child process terminates, only that process is restarted. This is the most common strategy.
  • `:one_for_all`: If a child process terminates, all other child processes are terminated, and then all children (including the original one) are restarted. This is used when the child processes are highly interdependent.
  • `:rest_for_one`: If a child process terminates, the rest of the child processes that were started *after* it are terminated and restarted along with the crashed child.

Choosing the right strategy is key to building fault-tolerant systems.

Read about Strategies in the official Elixir guide.

2. What is the difference between a `GenServer`, an `Agent`, and a `Task`?

  • `GenServer`: The workhorse of OTP. It’s a behaviour for implementing the client-server model. It maintains state, executes code asynchronously, and can handle synchronous calls (`call`) and asynchronous casts (`cast`).
  • `Agent`: A simple abstraction around a `GenServer` whose only purpose is to hold state. It provides a simpler API for getting and updating state (`Agent.get`, `Agent.update`).
  • `Task`: Used for running a one-off computation, often asynchronously. You can start a task and either await its result (`Task.await`) or run it as a “fire-and-forget” operation.

3. How does the BEAM’s preemptive scheduler work and why is it important?

The BEAM VM runs a preemptive scheduler, which means it doesn’t wait for a process to voluntarily give up control. Each process is given a small budget of “reductions” (a count of function calls). Once the budget is exhausted, the scheduler preempts the process and schedules another one to run. This ensures that no single process can monopolize a scheduler thread and starve others.

This is crucial for system responsiveness and fault tolerance, guaranteeing that even in a system with millions of processes, all processes get a fair chance to run.

Read more about the BEAM scheduler.

4. What are Elixir macros and how do they differ from functions? Provide a use case.

Macros are special functions that run at compile time. They receive code (in the form of its Abstract Syntax Tree, or AST) as arguments and must return code (another AST). This allows you to perform metaprogramming—writing code that writes code.

Functions, in contrast, run at runtime and operate on data values.

A common use case for macros is creating DSLs (Domain-Specific Languages) or reducing boilerplate. For example, the `test` macro in ExUnit is not a function; it’s a macro that transforms a block of code into a test function definition at compile time.

Read the Elixir guide on Macros.

5. What is a protocol in Elixir?

A protocol is a mechanism for achieving polymorphism in Elixir. It defines a set of functions that can be implemented for any number of different data types. For a given data type, the protocol will dispatch to the specific implementation for that type at runtime. If no implementation is found, it raises an error. This is how common functions like `Enum.count/1` can work on different data structures like Lists, Maps, and Ranges.

Phoenix Framework Internals

6. What is a `Plug` and how does the Plug pipeline work?

A **Plug** is a specification for a composable module that can be used to build web applications. At its core, a plug is a function that receives a connection struct (`%Plug.Conn{}`) and options, transforms the connection, and then must return the (possibly transformed) connection struct.

The **Plug pipeline** is a chain of these plugs. The initial connection from the webserver is passed to the first plug, its return value is passed to the second, and so on. This allows you to build up request handling logic in small, reusable steps for things like logging, authentication, and routing.

Read the official Plug documentation.

7. Explain the difference between an Endpoint and a Router in Phoenix.

  • Endpoint: The entry point for all requests into your application. It houses the initial plug pipeline that applies to *all* requests, handling things like static asset serving, request logging, session management, and CSRF protection before the request ever reaches the router.
  • Router: Receives the connection from the endpoint. Its job is to match the request’s path and HTTP verb to a specific controller action. It also defines smaller, scoped pipelines that apply to specific sets of routes (e.g., a `:browser` pipeline for session-based authentication and a `:api` pipeline for token-based authentication).

8. What is the purpose of Phoenix Contexts?

Contexts are dedicated modules that expose and group related functionality, acting as a boundary between your web layer (controllers, views) and your business logic. For example, instead of controllers calling Ecto functions directly, you would have a `Sales` context with functions like `Sales.create_invoice/1` or `Sales.list_customers/0`.

This approach helps keep the application organized, prevents the web layer from becoming bloated with business logic, and makes the core logic easier to test and reuse.

9. How do you configure a Phoenix application for a production environment?

Production configuration primarily happens in `config/runtime.exs` (for runtime settings) and `config/prod.exs` (for compile-time settings). Key steps include:

  • Configuring the `url` (host and port) in the Endpoint configuration.
  • Setting up the database connection pool size (`pool_size`).
  • Configuring the `secret_key_base` from an environment variable for security.
  • Setting `check_origin` to a list of allowed hosts to prevent cross-site attacks.
  • Compiling assets for production with `mix assets.deploy`.
  • Using Elixir releases (`mix release`) to create a self-contained, deployable artifact.

Real-Time Features (Channels & LiveView)

10. Describe the architecture of Phoenix Channels. What are transports, topics, and sockets?

Phoenix Channels provide a soft real-time communication layer over WebSockets or Long-polling.

  • Transports: The underlying connection mechanism. Phoenix defaults to WebSockets and falls back to Long-polling if WebSockets are unavailable.
  • Socket: A persistent connection from a client. In your `UserSocket` module, you handle authentication and identify the connection.
  • Topic: A named channel that clients can subscribe to (e.g., `”room:lobby”`). Messages are broadcast to all clients subscribed to a specific topic. The logic for handling messages on a topic is defined in a `Channel` module.
Read the official Phoenix Channels documentation.

11. Explain the Phoenix LiveView lifecycle.

The LiveView lifecycle manages the stateful connection between the client and server.

  1. `mount/3`: Called once when the LiveView is first rendered (both on the initial static render and the subsequent connected render). It’s used for initial state setup.
  2. `handle_params/3`: Called after `mount` and whenever the URL changes. It’s used to update state based on URL parameters.
  3. `handle_event/3`: Called when a client sends an event to the server (e.g., from a `phx-click` binding).
  4. `handle_info/2`: A callback for handling internal messages sent to the LiveView process.
  5. `render/1`: Called after `mount` and whenever the LiveView’s `assigns` change. It returns the rendered template. Phoenix intelligently sends only the diffs to the client.
See the LiveView Life-cycle documentation.

12. What is the difference between a stateful and a stateless event in LiveView?

This refers to how events are handled in relation to the LiveView process.

  • Stateful Event (`phx-click`, etc.): Sends an event over the WebSocket to the long-lived LiveView process on the server. This allows you to modify the state in the socket’s `assigns` and trigger a re-render.
  • Stateless Event (`phx-click-away`, `phx-focus`): These events are handled entirely on the client-side by JavaScript `Hooks`. They do not send an event to the server, and therefore cannot change the server’s state. They are used for purely UI-driven interactions like showing/hiding a dropdown.

13. What is Phoenix PubSub and how is it used by Channels and LiveView?

Phoenix PubSub is a built-in publish/subscribe system that allows processes to communicate across the entire application (and across multiple nodes in a distributed setup). When a client subscribes to a Channel topic, the channel process subscribes to a corresponding PubSub topic. When you broadcast a message from anywhere in your application (e.g., `MyApp.Endpoint.broadcast(“some_topic”, …)`), PubSub delivers that message to all subscribed channel processes, which then forward it to their connected clients. LiveView uses this same mechanism under the hood to update multiple clients when underlying data changes.

14. What is Phoenix Presence?

Phoenix Presence is a feature built on top of Channels that provides a simple way to track which users are connected to a specific topic. It uses a CRDT (Conflict-free Replicated Data Type) under the hood, which makes it eventually consistent and fault-tolerant, even in a distributed cluster. It efficiently tracks presences and broadcasts diffs to clients as users join or leave, making it ideal for building features like “who’s online” lists.

Ecto & Data Persistence

15. What is an Ecto Changeset and what are its primary functions?

An `Ecto.Changeset` is a data structure that represents a set of changes to be applied to your data. It provides a consistent way to handle data manipulation and validation. Its primary functions are:

  1. Filtering: Whitelisting the fields that are allowed to be changed (`cast`).
  2. Validation: Applying validation rules to the data (e.g., `validate_required`, `validate_length`).
  3. Transformation: Modifying the data before it’s persisted.
  4. Tracking Changes: It keeps track of what fields have changed and holds a list of validation errors.
Changesets are a core part of Ecto and are used for creating, updating, and validating data before it ever touches the database. Read the Ecto.Changeset documentation.

16. How does Ecto’s `preload` solve the N+1 query problem?

Similar to “eager loading” in other ORMs, `preload` allows you to fetch associated data for a set of parent records in a constant number of queries. When you call `Repo.preload(posts, :comments)`, Ecto first executes the query for the posts. It then collects all the `post_id`s and performs a *second* query to fetch all associated comments at once (e.g., `SELECT * FROM comments WHERE post_id IN (…)`). Finally, it maps the comments back to their parent posts in Elixir, avoiding the N+1 problem.

17. What is `Ecto.Multi` and what is it used for?

`Ecto.Multi` provides a way to compose multiple Ecto operations (inserts, updates, deletes, or custom functions) into a single, atomic database transaction. You build up a multi object by naming each step (e.g., `Multi.insert(:user, user_changeset)`). When you run it with `Repo.transaction(multi)`, all operations will be executed inside a transaction. If any single operation fails, the entire transaction is rolled back automatically. This is essential for maintaining data integrity when multiple related records must be changed together.

Explore the Ecto.Multi documentation.

18. What is the difference between `Ecto.Query.from` and `Ecto.Schema`?

  • `Ecto.Schema`: Used to map a database table to an Elixir struct. It defines the fields, types, and associations for your data. The resulting struct is what you work with in your application logic.
  • `Ecto.Query.from`: The starting point for building an Ecto query. It provides a DSL for creating queries that will be translated into SQL. While you often start a query from a schema (e.g., `from u in User`), you can also query without schemas or compose more complex queries.

19. How would you handle a many-to-many relationship with additional attributes on the join table?

You would define a dedicated schema for the join table itself. Instead of using a simple `many_to_many` association, you would treat the join table as a first-class citizen in your domain.

For example, for `posts` and `tags` with a `posts_tags` table that also has a `created_by` field, you would have a `PostTag` schema. The `Post` model would have a `has_many :post_tags` and `has_many :tags, through: [:post_tags, :tag]`. This allows you to create and query the join records with their extra attributes directly.

Architecture & Testing

20. Differentiate between an Umbrella application and a standard Mix project.

  • Standard Project: A single Mix project with its own applications, dependencies, and configuration.
  • Umbrella Project: A project that contains multiple distinct Elixir applications within a single codebase and `apps` directory. These applications can have dependencies on each other. This is useful for large projects where you want to separate different domains (e.g., a core app, a web app, and a reporting app) while still managing them in one repository.

21. How does Elixir’s approach to fault tolerance differ from traditional try/catch exception handling?

Traditional `try/catch` is defensive; it tries to anticipate and handle errors locally. Elixir’s philosophy, inherited from Erlang, is “let it crash.” Instead of defensively programming for every possible error, you write “happy path” code inside processes. These processes are supervised. If a process encounters an unexpected error and crashes, its supervisor will restart it to a known, clean initial state according to its defined strategy. This approach leads to simpler code and more robust, self-healing systems.

22. What is application configuration in Elixir and how does it work across different environments?

Configuration is handled primarily in the `config/` directory.

  • `config/config.exs`: The main configuration file, loaded first.
  • `config/{env}.exs` (e.g., `dev.exs`, `test.exs`): Environment-specific configuration that is loaded after and can override the main config.
  • `config/runtime.exs` (Elixir 1.11+): This file is evaluated when building a release, at runtime. It is the correct place to load secrets and environment-specific values from environment variables, as it runs on the target machine, not the build machine.

23. How do you test a `GenServer`?

You should test a `GenServer`’s public API, not its internal state or callbacks directly. In your test, you start the `GenServer` under the test’s supervision tree. Then, you call the client functions (e.g., `MyGenServer.get_state(pid)`) and assert that they return the expected results. You can send messages to the process and check its state changes indirectly through its public API. Avoid using `GenServer.call` or `cast` directly in tests.

24. What is the purpose of the Ecto Sandbox?

The Ecto Sandbox provides a way to run tests that interact with the database in parallel without them interfering with each other. For each test process, it checks out a connection from the database pool and runs all of that test’s database queries inside a single, isolated transaction. At the end of the test, the transaction is rolled back, ensuring a clean state for the next test. This allows for fast, concurrent, and reliable database testing.

25. How do Elixir releases (`mix release`) work and why are they preferred for deployment?

A release is a self-contained, deployable artifact that bundles your compiled Elixir code, all its dependencies, and a stripped-down version of the Erlang Run-Time System (ERTS). They are preferred over simply running `mix` in production because:

  • Self-contained: They don’t require Elixir or Mix to be installed on the target server.
  • Configuration: They provide a robust system for runtime configuration via `runtime.exs`.
  • Control: They come with scripts for starting, stopping, and connecting a remote shell to the running application.

Skip the interview marathon.

We pre-vet senior engineers across Asia using these exact questions and more. Get matched in 24 hours, $0 upfront.

Get Pre-Vetted Talent
WhatsApp