Skip to main content

Elixir engineer role-specific interview questions

30 questions on role-specific for elixir engineer candidates. Expect prompts such as “GenServer handle_call vs handle_cast tradeoffs” and “OTP supervisor restart strategies explained”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 25 of 30 role-specific questions (page 1 of 2).

As asked

When would you choose handle_call over handle_cast in a GenServer, and what are the failure modes you need to think about for each?

Sample answer outline

handle_call is synchronous, gives a reply, and lets the caller know if the server crashed (it gets an error). handle_cast is fire-and-forget, faster, but the caller has no visibility into failure or ordering. A strong answer mentions that using cast for state mutation without knowing if it succeeded can cause subtle bugs, and that call adds back-pressure naturally while cast can overwhelm a slow server.

Expect these follow-ups

  • How would you implement a timeout on a GenServer call, and what happens to the server when the caller times out?
  • Could you ever replace a synchronous call with a cast plus a separate monitor on the server? What would that buy you?
genserverotpconcurrencyback-pressure

As asked

Describe the difference between the :one_for_one, :one_for_all, and :rest_for_one supervisor strategies. Give me a concrete scenario where you would choose each one. How does DynamicSupervisor replace the old :simple_one_for_one strategy, and when would you reach for it?

Sample answer outline

A strong answer gives concrete examples: :one_for_one for independent workers like connection pool members, :one_for_all when child processes share state and a crash in one invalidates the others (e.g., a registry and its worker), :rest_for_one when children have a pipeline dependency where later children depend on earlier ones. For dynamically spawned homogeneous workers like per-user sessions, the answer should name DynamicSupervisor, which replaced :simple_one_for_one in Elixir 1.6 and OTP 20. DynamicSupervisor.start_child/2 is the idiomatic way to add children at runtime.

Expect these follow-ups

  • What are max_restarts and max_seconds, and what happens when a supervisor hits those limits?
  • How would you design a supervision tree for a Phoenix channel that manages per-user state?
otpsupervisorfault-toleranceprocesses

As asked

Explain how Phoenix LiveView decides what HTML to send over the WebSocket on each update. What are assigns, what is the diff algorithm doing, and how can poorly structured templates hurt performance?

Sample answer outline

LiveView tracks assigns in the socket, renders templates through a compile-time analysis that separates static and dynamic parts, and on each change only sends the dynamic segments that changed. A strong answer mentions that deeply nested or computed values embedded directly in templates bypass this optimization, and that using components and passing assigns explicitly allows finer-grained diffing.

Expect these follow-ups

  • How does the phx-update attribute on the client side affect how LiveView applies DOM patches?
  • When would you use a live component versus a function component in LiveView?
phoenixliveviewwebsocketperformancefrontend

As asked

You are reviewing a Phoenix endpoint that lists 100 blog posts and their authors. Someone wrote it with Repo.all(Post) followed by Repo.get(User, post.user_id) in a loop. How do you fix this, and what Ecto features make it idiomatic?

Sample answer outline

The fix is to use Ecto preloads: either preload in the original query with a join, or use Repo.preload/2 which batches the secondary query into a single IN clause. A strong answer distinguishes between query preloads (join-based, single query) and Repo.preload (separate query with IN), and knows when each is appropriate. Mentioning dataloader for GraphQL contexts is a bonus.

Expect these follow-ups

  • How would you detect N+1 queries automatically in your test suite or development environment?
  • When would a join-based preload be worse than a separate batch query?
ectodatabasesperformancepostgresqlqueries

As asked

How do you structure changeset pipelines in Ecto when you have different validation rules for create versus update, and how do you handle embedded schemas with their own changesets?

Sample answer outline

A strong answer shows using separate named functions (e.g., create_changeset/2 and update_changeset/2) that call a shared base changeset, using cast_embed/3 or cast_assoc/3 for nested schemas, and using with_action/2 to distinguish insert from update constraints. Knowing the difference between validate_ functions (pure) and constraint/3 functions (database-backed) is essential.

Expect these follow-ups

  • What is the difference between validate_required/3 and a NOT NULL database constraint from Ecto's perspective?
  • How do you unit test a changeset without hitting the database?
ectochangesetsvalidationschemasdatabases

As asked

Describe how Phoenix's request pipeline is built on Plug. What is a Plug, how does conn flow through a pipeline, and how would you write a custom authentication Plug that halts the connection?

Sample answer outline

A Plug is any module implementing init/1 and call/2, or a function taking conn and opts. Phoenix router, pipelines, and controllers are all Plug stacks. Calling Plug.Conn.halt/1 sets conn.halted to true, which stops further plugs from executing. A strong answer shows writing a module Plug that checks for a token in the Authorization header, assigns the current user, and halts with send_resp(conn, 401, ...) if not authenticated.

Expect these follow-ups

  • How do Phoenix pipelines differ from Plug.Builder pipelines?
  • What is the risk of calling Plug.Conn.halt without also sending a response?
plugphoenixauthenticationmiddlewarerequest-pipeline

As asked

Walk me through how the BEAM VM schedules Elixir processes. What is a reduction, how does preemption work, and what happens when a process exceeds its reduction budget?

Sample answer outline

A strong answer covers reductions as a unit of work (roughly one function call or built-in operation), the per-scheduler run queues, preemptive scheduling at reduction limits (default 4000), and how the process is suspended and re-queued without blocking the scheduler thread. Bonus points for mentioning dirty schedulers for NIF-heavy work and the impact of long-running NIFs on latency.

Expect these follow-ups

  • What are dirty schedulers and when would you use them?
  • How does the scheduler handle a process that is blocked waiting on a message versus one that is CPU-bound?
beamschedulerprocessesreductionsconcurrency

As asked

You need to store and look up a large, frequently-read, rarely-written in-memory table of data. Why might you choose ETS over a GenServer holding a map, and what tradeoffs does that choice carry?

Sample answer outline

ETS tables allow concurrent reads without going through a single process, eliminating the bottleneck of a GenServer serializing all reads. A strong answer covers table types (set, ordered_set, bag), concurrency modes (:public, :protected, :private), the lack of transactions across multiple keys, and the fact that ETS is not persistent across node restarts. Persistent term (:persistent_term) is worth mentioning for global read-mostly config.

Expect these follow-ups

  • What is :persistent_term and how does it differ from ETS?
  • How would you handle cache invalidation across multiple ETS tables consistently?
etsstateconcurrencyperformancebeam

As asked

You need to create a user, send a welcome email via a third-party API, and record an audit log entry, all atomically. How would you structure this with Ecto.Multi, and how do you handle the case where the email API call fails?

Sample answer outline

Ecto.Multi lets you build a pipeline of named operations that run inside a single database transaction. A strong answer acknowledges that external API calls inside a database transaction are an anti-pattern because holding a DB connection while waiting on an HTTP call risks connection exhaustion and the transaction cannot roll back a sent email. The idiomatic fix is to use an outbox pattern or enqueue a background job (Oban) after the transaction commits.

Expect these follow-ups

  • How does Ecto.Multi.run/3 differ from Ecto.Multi.insert/3?
  • If you had to use Oban for the email step, how would you ensure the job is only enqueued if the database transaction succeeds?
ectotransactionsdatabasesobanpatterns

As asked

A GenServer in production is getting slower and slower. You suspect its mailbox is growing. How would you diagnose this, and what are the common architectural causes and fixes?

Sample answer outline

Diagnosis: Process.info(pid, :message_queue_len) or :sys.get_status/1 for a running server, or observing via :observer or Telemetry. Common causes: too many callers sending casts to a slow consumer, blocking work inside callbacks. Fixes: add back-pressure by using call instead of cast, move slow work to Task processes spawned from handle_info, shard state across multiple GenServers, or use a poolboy/Poolex pool.

Expect these follow-ups

  • How would you add Telemetry instrumentation to a GenServer to track queue length over time?
  • What is the risk of spawning Tasks from a GenServer's handle_cast callback, and how do you link them safely?
genserverdebuggingperformanceobservabilityotp

As asked

Describe how two Elixir nodes communicate in a distributed cluster. What is the Erlang distribution protocol, how do nodes discover each other, and what are the security implications of the default setup?

Sample answer outline

Nodes connect over TCP using the Erlang Port Mapper Daemon (EPMD) for discovery by default. All communication uses a shared cookie as authentication. A strong answer calls out that the default cookie-based auth is weak (shared secret, no encryption without TLS distribution), that you can enable TLS distribution for security, and that in Kubernetes environments EPMD-free clustering via libcluster with DNS or Kubernetes API strategies is preferred.

Expect these follow-ups

  • How would you use libcluster to auto-discover nodes in a Kubernetes deployment?
  • What is the difference between a connected node and a visible node in Elixir distribution?
distributionclusteringsecurityerlangkubernetes

As asked

You are building a real-time multiplayer game. When would you use Phoenix Channels over LiveView, and are there scenarios where both belong in the same feature?

Sample answer outline

Channels are a lower-level primitive suited for bidirectional messaging patterns like multiplayer games where the client drives events frequently and the server does not own the UI. LiveView makes sense when the server owns the view and you want to avoid writing a separate client-side app. A strong answer notes that you can use both: LiveView for lobby/matchmaking UI and a Channel for the in-game low-latency event loop.

Expect these follow-ups

  • How does Phoenix Channel presence tracking work, and what consistency guarantees does it offer?
  • How would you handle a Channel disconnect and reconnect without losing game state?
phoenixchannelsliveviewwebsocketreal-time

As asked

How does the :telemetry library work, and how would you instrument a custom business operation in a Phoenix app to emit metrics that Prometheus can scrape?

Sample answer outline

telemetry:execute/3 emits an event with measurements and metadata to any attached handlers. A strong answer mentions :telemetry.attach or :telemetry.attach_many to wire handlers, using PromEx or TelemetryMetricsPrometheus to aggregate counters/summaries/histograms, and Phoenix's built-in telemetry events (phoenix.router.dispatch.start/stop). Also covers span patterns using telemetry span helpers.

Expect these follow-ups

  • How would you trace a request through multiple GenServer calls using OpenTelemetry in Elixir?
  • What is the overhead of emitting Telemetry events in a hot code path, and how do you measure it?
telemetryobservabilityprometheusphoenixmonitoring

As asked

You use Oban for background jobs backed by PostgreSQL. Walk me through how Oban prevents duplicate jobs, handles job failures, and ensures at-least-once delivery even if the BEAM node crashes mid-job.

Sample answer outline

Oban persists jobs to a Postgres table before returning success to the caller. It uses advisory locks on job IDs to prevent concurrent execution of the same job. On node crash, jobs remain in the table with state 'executing' and a rescue mechanism re-queues them after a timeout. A strong answer also covers the unique/4 option for deduplication, the max_attempts and backoff settings, and how to design idempotent workers.

Expect these follow-ups

  • How do you test Oban workers without running the full job queue in tests?
  • What is the Oban.Pro 'Engines' feature and when would you consider it?
obanbackground-jobspostgresqlreliabilitypatterns

As asked

A LiveView page needs to initialize a third-party chart library on mount and update it when server-side data changes, without losing chart state on each re-render. How do you implement this with LiveView JS hooks?

Sample answer outline

LiveView hooks allow attaching JavaScript lifecycle callbacks (mounted, updated, destroyed, etc.) to DOM elements via phx-hook. A strong answer covers defining the hook in app.js, using this.el to access the DOM node, using pushEvent to send events from JS to the server, and receiving updates via handleEvent. The phx-update='ignore' attribute on the chart container prevents LiveView from patching the DOM that the chart library manages.

Expect these follow-ups

  • How do you send data from a LiveView server event down to a specific hook without re-rendering the component?
  • What cleanup must a hook perform in its destroyed callback to prevent memory leaks?
liveviewjavascripthooksfrontendphoenix

As asked

You need to show all users currently viewing a shared document, across multiple nodes in your cluster. How does Phoenix.Presence work under the hood, and what consistency model does it use?

Sample answer outline

Phoenix.Presence uses a CRDT (conflict-free replicated data type) called a state-based CRDT under the hood, synced via PubSub across nodes. Each node owns the presence entries for connections on that node and gossips state. On merge, conflicts resolve by taking the union, so it is eventually consistent. A strong answer covers the Presence.track/3, Presence.list/2, and handle_diff callback, and notes that presence data should be treated as eventually consistent, not strongly consistent.

Expect these follow-ups

  • What happens to presence entries when a node goes down and comes back up?
  • How would you augment presence metadata with user avatar URLs without making a DB call on every join?
phoenixpresencecrdtdistributionreal-time

As asked

A search endpoint accepts optional filter parameters: name, status, inserted_after, and sort_by. How do you build the Ecto query incrementally without adding if-nil branches everywhere, and what is the idiomatic pattern for this in Elixir?

Sample answer outline

The idiomatic pattern is to build a base query and pipe it through filter functions that pattern-match on nil vs value, adding clauses only when the parameter is present. Using Enum.reduce over the params map with a function dispatch table is an alternative. A strong answer also mentions scoping query composition into a dedicated Filters module for testability and reuse.

Expect these follow-ups

  • How do you prevent SQL injection when accepting a user-supplied sort_by column name?
  • How would you add pagination (limit/offset or cursor-based) to this query?
ectoqueriescomposabilitypostgresqlpatterns

As asked

A user clicks a 'like' button in LiveView. The server roundtrip takes 150ms and the UI feels sluggish. How do you implement an optimistic update in LiveView so the button responds immediately, and how do you reconcile if the server call fails?

Sample answer outline

LiveView's JS module allows client-side attribute toggling (push with optimistic DOM mutation) before the server responds. A strong answer covers using JS.toggle or JS.set_attribute to flip the liked state immediately on click, then letting the server's push_event or assign update reconcile on completion. On failure, a push_event can signal the client hook or a LiveView event can reset the assign.

Expect these follow-ups

  • How does phx-disable-with work, and how is it different from an optimistic update?
  • What happens to an optimistic update if the WebSocket disconnects before the server responds?
liveviewfrontenduxphoenixoptimistic-updates

As asked

A process on a production node is behaving unexpectedly but you cannot redeploy. What tools do you reach for to inspect its state, trace its messages, and profile its reductions without taking the node down?

Sample answer outline

:sys.get_state/1 retrieves a GenServer's current state non-intrusively. :sys.trace/2 turns on tracing for a specific process. recon_trace (from the recon library) does safe rate-limited message tracing in production without flooding the scheduler. :erlang.process_info/2 gives mailbox length, heap size, and reductions. A strong answer mentions the risk of :dbg and :sys.trace on high-throughput processes and prefers recon_trace for production.

Expect these follow-ups

  • How would you safely trace all calls to a specific function across all processes in production using recon_trace?
  • What is the difference between :erlang.trace/3 and :dbg.tracer/0, and which is safer for production use?
debuggingobservabilityrecontracingbeam

As asked

What delivery guarantee does Phoenix.PubSub provide? If a subscriber is slow or temporarily offline, does it receive messages it missed, and how would you design around that limitation?

Sample answer outline

PubSub is fire-and-forget: it delivers to currently subscribed processes only, with no buffering or persistence. A slow subscriber does not get replayed messages after reconnect. A strong answer covers designing around this with a combination of PubSub for live updates and a database or event store for authoritative state that clients fetch on reconnect, or using a persistent queue like Oban or Kafka for events that must not be lost.

Expect these follow-ups

  • How would you implement a 'missed events since last seen' feature for a LiveView chat?
  • What happens to in-flight PubSub messages when a node leaves the cluster?
pubsubphoenixdistributionreliabilitymessaging

As asked

How does Dialyzer work in the context of an Elixir project, what is PLT, and what kinds of bugs does Dialyzer catch that ExUnit tests typically miss?

Sample answer outline

Dialyzer performs success typing analysis: rather than proving code correct, it proves code wrong when it can find a contradiction. The PLT (persistent lookup table) caches analyzed beam files. It catches type inconsistencies, dead code branches, calls to non-existent functions, and pattern matches that can never succeed. A strong answer mentions dialyxir as the Mix integration, the high initial PLT build cost, and that Dialyzer is sound but not complete (will not catch all bugs).

Expect these follow-ups

  • What is the difference between @spec and @type in Elixir?
  • When Dialyzer warns about a function that you know is correct, how do you suppress the warning without losing type safety?
dialyzertypespecsstatic-analysistestingelixir

As asked

You notice that LiveView processes on your server are growing in memory over time and never shrinking. Users have not noticed yet but memory is climbing. What steps do you take to diagnose and fix this before it becomes an incident?

Sample answer outline

Use :erlang.process_info(pid, :memory) and :sys.get_state/1 to inspect a live LiveView process. Check assigns for unbounded growth (lists, maps accumulating). Look for handle_info clauses that accumulate data without pruning, or subscriptions that deliver messages with no handle_info match (filling the mailbox). Fix by adding explicit cleanup in handle_info, resetting assigns, or pruning accumulating state. Set up a Telemetry metric on process memory for ongoing monitoring.

Expect these follow-ups

  • How would you identify which LiveView component or assign is growing without reading every socket's state manually?
  • What is the risk of calling :sys.get_state on a live production LiveView process?
liveviewdebuggingmemoryperformancebeam

As asked

You check your Oban dashboard and one queue has 200,000 pending jobs and is falling further behind. The queue normally processes in near real time. What do you do?

Sample answer outline

Diagnose first: is the backlog from a sudden spike or a slowdown in workers? Check worker error rate (are jobs failing and retrying?), check database connection pool utilization, check for a long-running job holding resources. Mitigation: temporarily increase concurrency for that queue, check for a poison pill job blocking others, pause non-critical queues to free resources. Long-term: add worker-level Telemetry, set up alerts on queue depth.

Expect these follow-ups

  • If the backlog is legitimate (a spike in new jobs), how do you drain it safely without overloading the database?
  • How do you differentiate between a poison pill job and a legitimately slow job?
obanbackground-jobsdebuggingperformancepostgresql

As asked

You need to add an index to a PostgreSQL table with 500 million rows in a running production system. How do you do this without locking the table and causing downtime, and how does this interact with your Ecto migration strategy?

Sample answer outline

Use CREATE INDEX CONCURRENTLY in the Ecto migration via execute/1. This avoids a full table lock but takes longer and can fail without cleanup, leaving an invalid index. Ecto migrations wrap in a transaction by default; disable that with @disable_ddl_transaction true and use concurrent: true on the index. A strong answer also covers the risk of a concurrent index failing (leaving an INVALID index that needs REINDEX), monitoring index build progress via pg_stat_progress_create_index, and blue-green deploy implications.

Expect these follow-ups

  • How do you handle a partially built invalid index if the migration fails halfway?
  • When adding a NOT NULL column with a default to a large table, what is the PostgreSQL 11+ optimization that avoids a table rewrite?
ectopostgresqlmigrationszero-downtimedatabases

As asked

Walk me through how hot code upgrades work in an OTP release. What files are involved, what constraints exist on process state, and when would you avoid hot upgrades in production?

Sample answer outline

A strong answer covers appup and relup files, the code_change/3 callback on GenServer for migrating state, the two-version code loading mechanism in the BEAM, and the constraint that you cannot change the module interface in incompatible ways during a hot upgrade. Most production teams avoid hot upgrades due to complexity, preferring blue-green deploys or rolling restarts, which is a valid and honest answer.

Expect these follow-ups

  • How does the BEAM support running two versions of a module simultaneously?
  • What is the role of the sys module in inspecting running GenServer state during a hot upgrade?
otpreleaseshot-upgradedeployment