Skip to main content

Elixir engineer technical fundamentals interview questions

11 questions on technical fundamentals for elixir engineer candidates. Expect prompts such as “Immutability and rebinding in Elixir” and “Pattern matching depth: pins, guards, and function heads”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 11 of 11 technical fundamentals questions.

As asked

Elixir values are immutable, but you can rebind variables. What does that actually mean at the BEAM level, and how does it affect how data is shared between processes?

Sample answer outline

Variables in Elixir are bindings to values, not containers. Rebinding (x = x + 1) creates a new binding, the old value is unchanged. Data passed between processes is copied (deep copy for heap terms), so there is no shared mutable state by design. Large binaries are an exception: they are reference-counted on a shared binary heap. A strong answer mentions that pattern matching is a destructure + assertion, not assignment.

Expect these follow-ups

  • What is the large binary heap in the BEAM and when does copying not occur?
  • How does immutability relate to the 'let it crash' philosophy?
elixirimmutabilitybeamprocessesfundamentals

As asked

Explain how pattern matching works in function heads in Elixir. What is the pin operator, what is a guard, and how does the compiler decide which function clause to use?

Sample answer outline

Function clauses are tried top to bottom; the first matching pattern wins. The pin operator (^) matches against the current value of a variable rather than rebinding it. Guards (when) add conditions beyond structural matching and are limited to pure expressions. A strong answer mentions that non-matching guards do not raise exceptions but fall through to the next clause.

Expect these follow-ups

  • What kinds of expressions are allowed in a guard clause and why are function calls restricted?
  • How does the compiler optimize a large set of function clauses with pattern-matched integer heads?
elixirpattern-matchingfundamentalsfunctions

As asked

What is an OTP behaviour in Elixir terms, how does @behaviour and use differ, and why does GenServer implement a behaviour rather than being a regular module?

Sample answer outline

A behaviour is a contract defined by a set of @callback specs. Implementing a behaviour means the module declares it with @behaviour and provides all required callbacks. use GenServer injects default implementations via __using__/1 macro, so partial implementation is possible. The OTP framework dispatches to the callback module's functions, enabling the generic server loop to be reused across different business logic.

Expect these follow-ups

  • What happens if you declare @behaviour GenServer but omit a required callback?
  • How does @impl true help with accidental callback misspellings?
otpbehaviourgenserverfundamentalsmodules

As asked

Explain what 'let it crash' means in practice. When should you use rescue or try/catch in Elixir, and when should you deliberately avoid it?

Sample answer outline

Let it crash means allowing unexpected errors to propagate and restart the process via its supervisor, rather than writing defensive rescue blocks that hide bugs. Use try/rescue at system boundaries: calling external services, parsing user input, interacting with the filesystem, wrapping third-party NIFs. Avoid it inside business logic where a crash signals a programming error that should be visible. A strong answer notes the difference between expected errors (return tagged tuples) and unexpected errors (let them crash).

Expect these follow-ups

  • How does the with macro relate to the let-it-crash philosophy for multi-step operations?
  • When would you use Process.flag(:trap_exit, true) instead of relying on a supervisor?
elixirotplet-it-crasherror-handlingfundamentals

As asked

A GenServer that batches analytics events starts building a mailbox with millions of messages during a traffic spike. How do you debug it and redesign it?

Sample answer outline

The diagnosis should inspect process info, message queue length, reductions, memory, and the shape of incoming messages before guessing. The redesign should avoid treating a GenServer as an unbounded queue: use bounded demand, partition work across multiple processes, and push durable backlog into Broadway, Oban, RabbitMQ, or Kafka depending on delivery requirements. Batching should be time and size bounded, with explicit shedding or backpressure when downstream is slow. A senior answer distinguishes CPU saturation from downstream latency and from a single hot key. The common mistake is adding more callers while leaving one serial bottleneck in place.

Expect these follow-ups

  • How would you expose mailbox length as an operational metric?
  • When is dropping events acceptable?
  • How would Broadway change the design?
genserverbackpressurebeam

As asked

How does the |> operator work in Elixir at the syntax level, and what are common mistakes Elixir developers make when piping into functions with arity greater than one, into anonymous functions, or into macros that do not follow standard argument conventions?

Sample answer outline

The pipe operator passes the result of the left side as the first argument to the function on the right. Common mistakes: using anonymous functions without wrapping in a capture (then/1 helper or (&func/1) syntax), forgetting that pipe passes to the first argument so if a function expects the target as the second argument you need a closure, and mixing pipe with multi-argument functions that need parentheses.

Expect these follow-ups

  • How do you pipe into an anonymous function without assigning it to a variable first?
  • Are there cases where the pipe operator makes code less readable, and when would you avoid it?
elixirpipefundamentalssyntaxstyle

As asked

What guarantees does Elixir's message passing provide? Is it ordered, reliable, exactly-once? What happens to a message sent to a dead process?

Sample answer outline

Within a single BEAM node, messages between two processes are ordered and delivered reliably (FIFO between the same pair). A message sent to a dead process is silently dropped with no error (unlike GenServer.call which monitors the target and raises). Across distributed nodes, messages may be lost if the network link breaks. No exactly-once guarantee at the message primitive level. A strong answer distinguishes local from distributed semantics.

Expect these follow-ups

  • How does send/2 differ from GenServer.call/2 in terms of error detection?
  • What is the mailbox, and what is the default behavior when a process receives a message it has no receive clause for?
elixirprocessesmessagingfundamentalsdistribution

As asked

How does garbage collection work in the BEAM, and why is per-process GC a significant advantage over a global stop-the-world collector?

Sample answer outline

Each BEAM process has its own heap and is garbage collected independently using a generational semi-space algorithm. GC pauses only that one process for a few microseconds, not the whole system. This is why Elixir systems can have millions of processes with predictable low-latency: no global GC pause. A strong answer also mentions the process dying as the ultimate GC (all heap memory is reclaimed immediately), and the binary heap as the one exception to per-process isolation.

Expect these follow-ups

  • How do long-lived processes with large heaps behave differently during garbage collection?
  • What is the fullsweep_after GC option and when would you tune it?
beamgarbage-collectionmemoryperformancefundamentals

As asked

Explain the difference between a protocol and a behaviour in Elixir. Give me a concrete example of something you would model as a protocol and something you would model as a behaviour, and explain why.

Sample answer outline

Protocols enable polymorphism on data types (dispatch based on the value's type), similar to type classes. Behaviours define a contract for a module's interface (dispatch based on module identity, usually injected at runtime). Use Protocol for 'how does this type support this operation' (Enumerable, String.Chars, Jason.Encoder). Use Behaviour for 'what interface must this module implement' (GenServer, Plug, Oban.Worker). A strong answer notes protocols support open extension (any module can implement them without modifying the original).

Expect these follow-ups

  • How do you implement a protocol for a struct defined in a third-party library?
  • What is protocol consolidation and why does it matter for production performance?
elixirprotocolsbehaviourspolymorphismfundamentals

As asked

What is the difference between a binary and a bitstring in Elixir? What are reference-counted binaries, heap binaries, and sub-binaries, and why do sub-binaries matter for memory usage?

Sample answer outline

A bitstring is any sequence of bits; a binary is a bitstring with a length divisible by 8. Binaries under 64 bytes are heap binaries (copied with the process). Larger binaries live on a shared binary heap (reference-counted) and only a small reference is copied between processes. Sub-binaries (created by binary_part or pattern matching) hold a reference to the original large binary, preventing it from being garbage collected even if only a small portion is needed.

Expect these follow-ups

  • How do you force a sub-binary to own its data and release the parent binary from memory?
  • When building a large binary by repeated concatenation, how does the compiler optimize this with iodata?
elixirbinariesmemorybeamfundamentals