Skip to main content

Tempus AI

Elixir engineer at Tempus AI

The typical process shape, common elixir engineer interview questions, and a market salary band for this role.

Process timeline

  1. 1

    Recruiter screen

    A conversation about your background, motivation, and logistics, plus a first check that your experience lines up with the elixir engineer opening.

  2. 2

    Technical phone

    A remote session on the core skills of the role, worked through live with an interviewer rather than answered as trivia.

  3. 3

    Onsite

    A longer block of back-to-back interviews with the team, mixing deeper skill exercises with behavioral questions about how you work.

  4. 4

    Decision

    Interviewers pool their feedback, the hiring manager makes the call, and the recruiter comes back with an outcome or next steps.

Interview questions

Common elixir engineer interview questions that come up across companies like Tempus AI.

As asked

Design the OTP supervision tree for a Phoenix service that handles chat rooms, user presence, and outbound webhooks. What processes exist and what restarts when something fails?

Sample answer outline

A strong answer starts with process boundaries, not modules. Put room processes under a DynamicSupervisor so rooms can start and stop independently, and isolate webhook delivery workers so a bad third-party endpoint cannot take down chat state. Choose restart strategies deliberately: one_for_one for independent room workers, rest_for_one only where later children depend on earlier ones. Presence should be treated as derived state that can be rebuilt, while durable chat messages belong in the database. Candidates often trip up by making one GenServer own too much state or by restarting a whole subtree for a local failure.

Expect these follow-ups

  • When would you use Registry versus pg for finding room processes?
  • How do you avoid a thundering restart loop after a deploy?
  • What state is safe to keep only in a process?
otpsupervisionphoenix

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

Salary band

Use the salary page linked below for elixir engineer bands by city, modeled from BLS, ONS, and Levels.fyi reference medians.

View salary detail

Similar companies