Skip to main content

Scala engineer technical fundamentals interview questions

11 questions on technical fundamentals for scala engineer candidates. Expect prompts such as “Compare Future with an effect system” and “Referential transparency and its practical meaning in Scala”, 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

In Scala, when would you use Future directly, and when would you reach for an effect system such as cats-effect or ZIO?

Sample answer outline

Future starts running eagerly and is tied to an ExecutionContext, which is fine for simple service calls but makes cancellation, resource safety and deterministic tests harder. Effect systems represent work as values, so execution, retries, cancellation, timeouts and resource scopes can be composed before the program is run. The tradeoff is complexity and team learning cost. A strong answer names the operational benefits: bounded concurrency, structured resource handling and better testing with controlled runtimes. Candidates often describe effect systems as only functional style rather than as production control over side effects.

Expect these follow-ups

  • Why does eager execution make Future awkward in tests?
  • How does Resource prevent leaks?
  • When would an effect system be overkill?
futurecats-effectzio

As asked

Define referential transparency in your own words. Give me a concrete example of a Scala expression that is referentially transparent and one that is not, and explain why the distinction matters for testing.

Sample answer outline

An expression is referentially transparent if it can be replaced by its value without changing the program's meaning. 1 + 1 can always be replaced by 2. println('hello') cannot be replaced by () because the effect of printing is lost. For testing, RT code is trivially testable: no mocks needed, same inputs always give same outputs, and the test is deterministic. IO-based effects make side effects explicit but not impure at the expression level. A strong candidate explains how IO[A] is RT (it is a description, not the effect) while Future[A] is not.

Expect these follow-ups

  • Is IO[A] from Cats Effect referentially transparent? Explain your reasoning.
  • Why does Future fail the referential transparency test even though it wraps a computation?
referential-transparencyfunctional-programmingtestingtechnical-fundamentalsscala

As asked

What is the distinction between an effect and a side effect in functional programming, and how does Scala's IO monad make effects explicit without making them impure?

Sample answer outline

A side effect is an observable change (mutation, IO, network) that happens as a by-product of a computation, outside the type signature. An effect is the same concept but tracked in the type: IO[A] says 'this computation may perform IO and produces A.' The key insight is that IO[A] is a pure value (a description) and composing IO values is pure. Only running the IO (at the end of the world, in main) performs the effect. This makes code testable without executing real effects and enables safe composition.

Expect these follow-ups

  • What does 'end of the world' mean in the context of functional effects?
  • How do you test a function that returns IO[A] without actually performing the IO?
effectsside-effectsfunctional-programmingiotechnical-fundamentals

As asked

A consumer reads from Kafka, writes to a database and then commits offsets. Explain at-least-once, at-most-once and exactly-once behaviour in this flow.

Sample answer outline

If the consumer commits before the database write, a crash can lose the message, which is at-most-once. If it writes first and commits after, a crash between those operations can replay the message, which is at-least-once. Exactly-once for external side effects requires idempotent writes, transactions that include both offset and output, or a design where replay is harmless. Kafka transactions help when consuming and producing back to Kafka, but they do not magically make an arbitrary database update exactly once. Strong candidates discuss idempotency keys and deduplication tables.

Expect these follow-ups

  • Where would you store an idempotency key?
  • How do Kafka transactions work when the sink is another Kafka topic?
  • What is the operational cost of exactly-once processing?
offsetsdelivery-semanticsidempotency

As asked

The JVM does not support tail call optimization in general. Why not, and how does Scala work around this limitation with the @tailrec annotation and with trampolining?

Sample answer outline

The JVM bytecode specification defines a call stack model where each method call pushes a new frame. There is no TCO instruction in the JVM bytecode set, so the compiler cannot eliminate the frame for a tail call at the bytecode level. Scala's @tailrec annotation instructs the Scala compiler to verify that a recursive call is in tail position and rewrite it as a goto (while loop) in the generated bytecode, which avoids pushing new frames and prevents StackOverflowError. For mutual recursion (function A calls B calls A), @tailrec cannot help because the rewrite would need to span two methods. Trampolining solves this by returning a thunk (a Bounce or Done ADT) and running a driver loop that evaluates thunks iteratively until it gets a final result, keeping the call stack depth constant at the cost of heap allocation per step.

Expect these follow-ups

  • Cats provides a Trampoline type. How does it differ from a hand-rolled trampoline?
  • How does Scala 3's tailrec behave differently from Scala 2 for mutual recursion?
tail-recursionjvmtrampoliningtechnical-fundamentalsscala

As asked

What are the laws that a Monoid instance must satisfy, and why do these laws matter in practice for distributed computation (like Spark)?

Sample answer outline

Monoid laws: identity (combine(a, empty) == a and combine(empty, a) == a) and associativity (combine(combine(a, b), c) == combine(a, combine(b, c))). Associativity matters because it enables parallel reduce: split the data, reduce each partition independently, then combine the results. If your combining operation is not associative (like average of averages), you get wrong answers. A strong candidate gives a concrete broken example: averaging numbers is not associative, so you must track sum and count as a Monoid pair.

Expect these follow-ups

  • How do you verify that a Monoid instance satisfies the laws in a test suite using Cats Laws and ScalaCheck?
  • What is the difference between Monoid and CommutativeMonoid?
monoidtype-class-lawsfunctional-programmingtechnical-fundamentalsspark

As asked

Compare lazy val, by-name parameters, and LazyList in Scala. What are the differences in when each evaluates and how many times?

Sample answer outline

lazy val evaluates once on first access and caches the result. A by-name parameter (=> A) evaluates every time it is referenced inside the function. LazyList (formerly Stream) evaluates each element once on first access and memoizes it, enabling infinite sequences. A common mistake is using a by-name parameter when you expect caching, resulting in the expression evaluated multiple times. A strong candidate gives a concrete example of each and knows that lazy val uses double-checked locking on the JVM for thread safety.

Expect these follow-ups

  • What is the thread-safety model of lazy val on the JVM?
  • How would you implement memoization for a recursive function in Scala using lazy val or a mutable Map?
lazy-evaluationscalalazy-valby-nametechnical-fundamentals

As asked

What are opaque types in Scala 3 and how do they improve on Scala 2 value classes (AnyVal) for type-safe wrappers? What are the limitations of each?

Sample answer outline

Opaque types are aliases visible only in the defining scope, so outside the scope the alias is opaque (the underlying type is hidden). Unlike value classes, opaque types have zero runtime cost even when used as generic type arguments (AnyVal boxes when used as a generic). Opaque types cannot be extended, have no runtime class overhead, and the compiler enforces the abstraction. Limitation: you cannot pattern match on the underlying type outside the definition scope. Value classes still appear in some Scala 2 codebases as the closest equivalent.

Expect these follow-ups

  • How do you define extension methods on an opaque type in Scala 3?
  • Can you put an opaque type into a collection without boxing?
opaque-typesscala3value-classestype-systemtechnical-fundamentals

As asked

Scala's type system is nominal, not structural by default. What does this mean? When does Scala use structural typing, and what are the performance implications of structural types?

Sample answer outline

Nominal typing means two types are the same if they have the same name, not if they have the same structure. Scala supports structural types (refinement types like { def close(): Unit }), which use reflection at runtime for dispatch. The performance implication is that structural type method calls use reflection, which is orders of magnitude slower than virtual dispatch. This is why structural types are avoided in hot paths. A strong candidate also mentions that Scala 3's opaque types and type aliases give the nominal typing benefits without boxing overhead.

Expect these follow-ups

  • How does Scala 3 change structural typing compared to Scala 2?
  • When would you intentionally use a structural type in production Scala code?
type-systemstructural-typingscalatechnical-fundamentalsperformance

As asked

Explain the contramap operation and when you would use it in a Cats codebase. Give a concrete example with an Encoder or Ordering type class.

Sample answer outline

Contramap works on Contravariant functors where the type parameter appears in an input position. Given a Comparator[B] and a function A => B, contramap gives you a Comparator[A]. Concretely: if you have an Encoder[String] and a function User => String, you can contramap to get an Encoder[User]. This avoids writing the encoding logic from scratch. A strong candidate explains why Encoder is contravariant while Decoder is covariant and gives a memory aid: contramap works when you can transform the input.

Expect these follow-ups

  • What is the Contravariant type class in Cats and what is its primary operation?
  • How does contramap compose with map when you have a Profunctor?
contramapcatscontravarianttype-classestechnical-fundamentals

As asked

What is a path-dependent type in Scala? Give me an example where two instances of the same class have incompatible types despite being the same nominal type.

Sample answer outline

In Scala, inner types of an instance are path-dependent: outer1.Inner and outer2.Inner are different types even if both outer objects have the same class. This is useful for enforcing that operations on related objects stay paired (like a key from a specific Map). The type Graph#Node versus graph.Node illustrates the difference: Graph#Node (type projection) accepts any node, while graph.Node (path-dependent) only accepts nodes from that specific graph instance. A strong candidate shows a compile error from mixing path-dependent types.

Expect these follow-ups

  • What is the difference between type projection (#) and a path-dependent type (.) in Scala?
  • How do you use path-dependent types to encode invariants that the compiler enforces?
path-dependent-typestype-systemscalatechnical-fundamentalsadvanced