Scala Interview Questions

Reviewed by Mark Dickie · Last updated

13 questions on this page you can answer and see marked on the spot. Go to the first one

Scala is a statically typed JVM language that blends object-oriented and functional programming. Interviews tend to probe your grasp of pattern matching, the type system, and Scala's collections API. You should be comfortable with case classes, sealed traits, tail recursion, variance annotations, and the implicits system (or its Scala 3 replacement with given and using). Concurrency with Future and basic type-class design come up regularly at mid-level and above.

What topics come up in a Scala interview?

AreaWhat they testCommon question shapes
Pattern matchingExhaustiveness, sealed hierarchies, guardsMatch a sealed ADT, catch non-exhaustive warnings
Type systemVariance, bounds, higher-kinded typesWhy is List covariant but Function1 contravariant in its argument?
Implicits / givenResolution rules, type classesImplement Show or Eq without a library
Collectionsmap, flatMap, filter, performance traitsRewrite an imperative loop with foldLeft
ConcurrencyFuture, ExecutionContext, error handlingCompose two futures and handle failure

How should I prepare for a Scala interview?

  1. Write several small ADTs with sealed trait + case class members and practice exhaustive matching against them.
  2. Study variance by annotating your own generic classes and predicting which positions are covariant or contravariant.
  3. Build a type class from scratch (for example, a minimal JsonWriter) using Scala 2 implicits and then port it to Scala 3 given instances.
  4. Drill Future composition: map, flatMap, zip, recover, and traverse from the Cats library.
  5. Review tail recursion and the @tailrec annotation so you can explain when the compiler will reject a recursive method.

Key facts

  • Tarmac has 100 Scala interview questions on this topic, 25 of them on this page, at difficulty 1–5 of 5.
  • Tarmac tracked 115 job postings asking for Scala in September 2026.
  • Roles asking for Scala advertise a median base salary of £67,500, across 43 job postings as of September 2026.
  • Tarmac last reviewed these Scala interview questions on 20 October 2026.

At a glance

Questions25 shown · 100 in the bank
Difficulty1–5 of 5
FormatsMultiple choice, True / false, Ordering, Fill in the blank, Flashcard, Short answer, Code output, Multiple answer, Find the bug

What you'll review

  1. scala case classes equality
  2. scala objects companions
  3. scala traits mixins
  4. scala lazy collections
  5. scala akka actors
  6. scala collection ops

Practice questions

Try one before you open the answer. Pick an option and press Check; it's marked on the spot.

In Scala, given two case class instances created with identical field values, what does == return?#

Options

Show answer

true — Scala case classes generate a structural equals that compares field values, so two instances with identical fields are equal regardless of being separate objects.

Why:

Case classes automatically generate a value-based equals method. Since p1 and p2 have the same x and y values, p1 == p2 evaluates to true. Reference identity is not used; the comparison is structural.

In Scala, what is the result of comparing two case class instances of different case class types that happen to have the same field names and values?#

Options

Show answer

false — case class equality first checks that both objects are the same runtime type. Person and Employee are different types, so their instances are never equal even when field values match.

Why:

The auto-generated equals for a case class first checks that.isInstanceOf[Person]. Since Employee("Alice") is not an instance of Person, the equality check short-circuits to false. Field names and values matching is irrelevant when the runtime types differ.

In Scala, what does the following comparison print?#

Options

Show answer

false — case class equality compares every declared field. Even though the name values match, the age values differ (30 vs 31), so the two instances are not equal.

Why:

Case class equality compares ALL fields in declaration order. u1 and u2 have the same name ("Bob") but different age values (30 vs 31), so equals returns false and the output is false.

In Scala, a class and its companion object must share the same name and be defined in the same compilation unit. True or False?#

Options

Show answer

True. In Scala, a class and its companion object must share the same name and be defined in the same compilation unit. The Scala Language Specification uses the term 'compilation unit' (concretely, a single source file) rather than merely the same package, and satisfying these two conditions is what grants mutual access to private members.

Why:

The Scala Language Specification requires a companion object to have the same name as the class and to be defined in the same compilation unit (typically a single source file). When both conditions are met, the class and the companion object can access each other's private members.

In Scala, a class can extend multiple traits using the extends keyword for the first trait and the with keyword for each additional trait. True or False?#

Options

Show answer

True. In Scala, a class mixes in multiple traits by listing the first one after extends and each additional one after with, for example class C extends T1 with T2 with T3. This linearization-based composition is the standard mechanism for combining traits.

Why:

Scala allows a class to mix in multiple traits. The first trait (or class) is referenced with extends, and every subsequent trait is added with with. This is the standard syntax for trait composition in Scala.

Given the following Scala class hierarchy, order the user-defined types from most-derived (first in linearization) to least-derived (last in linearization).#

Put these in order

Show answer

The correct linearization order from most-derived to least-derived is Cat → FourLegged → Furry → Animal. Scala's linearization processes mixins right-to-left: the rightmost trait in the with clause (FourLegged) appears first after the class itself, followed by Furry, with the shared base class Animal appearing last after duplicate elimination.

Why:

Scala's linearization formula for C extends B1 with B2 with ... with Bn is L(C) = C, L(Bn) ⊕ ... ⊕ L(B1), where ⊕ merges sequences giving priority to the right operand (elements that appear in both are kept from the right operand). For Cat extends Animal with Furry with FourLegged, the parents are B1=Animal, B2=Furry, B3=FourLegged, so we compute L(FourLegged) ⊕ L(Furry) ⊕ L(Animal). Each trait's linearization is: L(Animal)=[Animal], L(Furry)=[Furry, Animal], L(FourLegged)=[FourLegged, Animal]. Merging L(FourLegged) ⊕ L(Furry): FourLegged is unique to the left, Animal is shared so it comes from the right operand, yielding [FourLegged, Furry, Animal]. Merging that with L(Animal) changes nothing since Animal is already present. The full linearization is therefore: Cat → FourLegged → Furry → Animal. FourLegged appears before Furry because it is the rightmost mixin in the with clause, so its linearization is processed first in the merge chain.

In Scala 2.13+, to create a lazily-evaluated sequence of integers starting from 1, you write:#

Show answer

In Scala 2.13+, to create a lazily-evaluated sequence of integers starting from 1, you write:

val nums = **LazyList**.from(1)

Fill in the blank with the standard library type name (a single CamelCase identifier).

Why:

LazyList is the standard lazy (non-strict) immutable sequence introduced in Scala 2.13 to replace the deprecated Stream. Its elements are computed only when accessed, which is why LazyList.from(1) can represent an infinite sequence without evaluating every element.

In Scala 2.13+, to defer operations like map and filter on a List so they are not evaluated until a forcing operation is applied, you call the method ._____ on the collection:#

Show answer

In Scala 2.13+, to defer operations like map and filter on a List so they are not evaluated until a forcing operation is applied, you call the method .**view** on the collection:

val result = List(1, 2, 3, 4).**view**.map(_ * 2).filter(_ > 4).toList

Fill in the blank with the single-word method name.

Why:

Calling .view on a strict collection produces a SeqView (a lazy view). Transformations applied to the view — such as map and filter — are not executed immediately; they are composed and deferred until a forcing operation like .toList is called, which traverses the view and produces a new strict collection.

In Akka Typed, what is the primary mechanism by which actors communicate with each other, and what guarantees this communication is safe for concurrency?#

Show answer

Actors communicate by sending asynchronous messages to each other's mailbox using the ! (tell) operator, which is fire-and-forget. The concurrency safety comes from actor isolation: the framework guarantees each actor processes one message at a time from its mailbox, so no shared mutable state is accessed concurrently and no explicit locking is needed. Messages should be immutable as a best practice, but the framework does not enforce immutability — the safety property is the single-message-at-a-time processing guarantee.

Why:

In Akka Typed the primary communication primitive is the tell operator (!), which places a message in the recipient's mailbox without waiting for a reply. The ask pattern (ask via AskPattern) is a separate, higher-level abstraction that returns a Future and is built on top of tell, not a built-in operator on ActorRef. The key concurrency-safety guarantee is actor isolation: the framework ensures each actor handles exactly one message at a time, eliminating the need for explicit synchronization. While immutability of messages is a strongly recommended best practice, it is not enforced by the framework — the real safety property is the serialized message processing within each actor.

In Scala, a class and its companion object share the same name and must be defined in the same source file. What specific access privilege do the class and the companion object grant to each other that is NOT available to unrelated classes or objects?#

Show answer

They can access each other's private members (private fields, private methods, and private constructors).

Why:

A companion object and its companion class can see each other's private members. This is the defining benefit of the companion relationship in Scala and is why companion objects are commonly used to hold factory methods that invoke private constructors, or to hold shared private state that the class instances can read.

In Scala, when a trait invokes super.someMethod() and that trait is mixed into a class alongside other traits that also override someMethod, what mechanism does Scala use to determine which implementation of someMethod is actually called?#

Show answer

Scala uses linearization. The traits are linearized into a single order (right-to-left, with the class itself last), and super calls resolve to the next trait in that linearization order.

Why:

Scala resolves trait super calls through linearization — a deterministic right-to-left ordering of all mixed-in traits. When a trait calls super.someMethod(), the call is dispatched to the next trait in the linearization chain, not to the JVM parent class directly. This is what enables stackable modifications and traits to work as composable layers.

When you declare a case class in Scala, the compiler automatically synthesizes a companion object for it. Name the two primary methods the compiler generates in that companion object that enable pattern matching and construction without new.#

Show answer

The compiler generates an apply method (so instances can be created without new) and an unapply method (so the case class can be used in pattern matching as an extractor).

Why:

The case class companion object receives a synthesized apply method that acts as a factory (allowing MyCase(1, 2) instead of new MyCase(1, 2)) and a synthesized unapply method that deconstructs an instance back into its fields for pattern matching. These two methods are the core of why case classes work seamlessly with match expressions and constructor-call syntax.

Consider the following Scala trait hierarchy:#

Put these in order

Show answer

The linearization order of D extends B with C is D → C → B → A. Scala builds the linearization by starting with the class, then prepending the linearizations of the mixins from right to left, removing duplicates. So super.greet() in D calls C, which calls B, which calls A, producing "D->C->B->A".

Why:

Scala computes the linearization of D extends B with C as: start with D, then prepend the linearization of the rightmost mixin C (giving C, A), then prepend B's linearization (giving B, with A already present so it is removed). The result is D, C, B, A (followed by AnyRef, Any). When greet() is called, D.greet invokes super which resolves to C, which resolves to B, which resolves to A. So the invocation order from most-derived to least-derived is D → C → B → A.

Consider the following Scala class with mixed-in traits:#

Put these in order

Show answer

The constructor output order is Base → T1 → T2 → T3 → Derived. Scala initializes the superclass first, then trait initializers left-to-right in the with clause order, and finally the class body itself, matching the linearization order from least-derived to most-derived.

Why:

In Scala, the superclass constructor (Base) runs first, then the trait initializers execute in the order they are declared left-to-right (T1, T2, T3), and finally the body of the class itself (Derived) runs last. This mirrors the linearization order with the most-derived element (Derived) initialized last. The output is: Base, T1, T2, T3, Derived.

What does the following Scala code print? Assume Scala 2.13+ (LazyList from the standard library).#

var count = 0
val lzy = LazyList.from(1).map { x => count += 1; x * 2 }
val a = lzy.head
val b = lzy.head
println(count)
Show answer
1
Why:

LazyList.from(1) produces an infinite lazy sequence starting at 1. The .map call is lazy — it returns a new LazyList without evaluating anything yet. The first lzy.head forces evaluation of the first element: the map function runs once (count becomes 1) and the result (2) is memoized in the LazyList cell. The second lzy.head returns the already-computed, cached head without re-invoking the map function, so count stays at 1. This memoization behavior is the key distinction between LazyList and a non-caching lazy view that recomputes on each traversal.

Given the following case class definition in Scala 3:#

Options

Pick every one that applies.

Show answer

The compiler generates equals (structural equality on constructor fields), hashCode (consistent with equals), canEqual (used by the generated equals to let subclasses control cross-type equality), and copy (a new instance with optional field overrides). It does not generate compareTo; natural ordering requires Ordered or an explicit Ordering.

Why:

For every case class the Scala compiler generates: equals (structural, based on constructor parameters), hashCode (consistent with equals), canEqual (invoked by the generated equals so subclasses can opt out of cross-type equality), and copy (creates a new instance with named-parameter overrides for any subset of fields). A compareTo method is NOT generated — natural ordering requires mixing in Ordered or supplying an Ordering instance.

Consider the following Scala code:#

Options

Pick every one that applies.

Show answer

After the mutation, m1 == m2 is false (the generated equals compares field values which now differ), m1.hashCode changed (the generated hashCode is derived from value), and using a case class with var fields in a hash-based collection after mutation breaks the hashCode/equals contract. set.contains(m1) is NOT guaranteed to return true because the changed hashCode places the element in the wrong bucket. The generated equals compares values, not references.

Why:

The compiler-generated equals and hashCode for case classes are based on constructor parameter values, not references. After m1.value = 2, m1 has value 2 while m2 still has value 1, so m1 == m2 is false. The generated hashCode is derived from value, so it also changed. Because the hashCode changed, m1 is now stored in the wrong bucket in the HashSet, meaning set.contains(m1) is not guaranteed to find it — this breaks the hashCode/equals contract required by hash-based collections. The generated equals compares field values, not references, so the claim that the generated equals compares object references rather than field values is false.

What does the following Scala code print when executed?#

trait Base {
  def greet: String = "Hello"
}
trait A extends Base {
  override def greet: String = "A(" + super.greet + ")"
}
trait B extends Base {
  override def greet: String = "B(" + super.greet + ")"
}
trait C extends Base {
  override def greet: String = "C(" + super.greet + ")"
}
class Concrete extends Base with A with B with C
val obj = new Concrete
println(obj.greet)
Show answer
C(B(A(Hello)))
Why:

The class class Concrete extends Base with A with B with C produces the linearization: Concrete → C → B → A → Base (traits listed right-to-left in the declaration end up left-to-right just below the class). When greet is called, dynamic dispatch resolves to the leftmost override in the linearization—C.greet. Each trait calls super.greet, which resolves to the next trait to the right. So C calls B, B calls A, and A calls Base, yielding C(B(A(Hello))).

The function below is intended to return all positive integers from the input list, preserving their relative order. For input List(1, 2, -1, 3, 4) it should return List(1, 2, 3, 4), but it returns List(1, 2). Which line contains the bug?#

def keepPositives(xs: List[Int]): List[Int] = {
  val positives = xs.takeWhile(_ > 0)
  positives
}
Show answer

The bug is on line 2.

Why:

takeWhile stops scanning as soon as it hits the first element that fails the predicate, so List(1, 2, -1, 3, 4).takeWhile(_ > 0) returns only List(1, 2) — the elements after the -1 are never examined. To keep every positive element regardless of position, the function should use filter(_ > 0) on line 2. The other lines (the function signature, the val binding, and returning positives) are all correct.

The function below should count how many times each word appears in the list. For input List("a", "b", "a") it should return Map("a" -> 2, "b" -> 1), but it returns Map("a" -> 3, "b" -> 2). Which option correctly identifies the bug?#

def countOccurrences(words: List[String]): Map[String, Int] =
  words.foldLeft(Map.empty[String, Int]) { (acc, w) =>
    acc + (w -> (acc.getOrElse(w, 1) + 1))
  }

Options

Show answer

Line 3 — the default value in getOrElse should be 0 instead of 1

Why:

Tracing the fold for List("a", "b", "a"): the first time a word is seen it is absent from the map, so getOrElse(w, 1) returns the default 1, and + 1 makes the count 2 instead of 1. Every subsequent occurrence also adds 1 on top of the already-inflated value. The fix is getOrElse(w, 0) so the first occurrence produces a count of 1. Using foldRight instead of foldLeft is wrong because counting is order-independent. Using ++ instead of + is wrong because + is the correct Map method for adding/updating a single key-value pair; ++ merges an entire Map. Using Map(words.head -> 0) as the seed is wrong because an empty seed map is correct and words.head would fail on an empty list.

Consider the following Scala 3 code (behavior is identical on Scala 2.13+):#

Options

Pick every one that applies.

Show answer

t1 == t3 and t1 == t4 evaluate to true. Scala case-class equals compares each field with ==, but Array inherits Java's reference-based Object.equals, so two separately-allocated arrays with the same contents are never equal. Both t1.copy() and Team("A", t1.members) reuse t1's array object, making those comparisons true; t1 == t2 fails because its arrays are distinct objects, and eq checks instance identity.

Why:

Case classes generate an equals that compares each constructor parameter via ==. For the members field this delegates to the array's equals, which is Java's reference-based Object.equals (arrays do not override equals). So t1 == t2 is false: the two Array("x","y") literals are distinct objects, and array equals is reference identity — the names match but the array references differ. t1.copy() allocates a new Team but reuses the same Array reference for members, so t1 == t3 is true: t1.name == t3.name is true and t1.members == t3.members is true because it is the same array object compared to itself. t4 = Team("A", t1.members) also passes the exact same array reference, so t1 == t4 is true for the identical reason — same name, same array object. t1 eq t3 is false because eq is reference equality on the Team instances themselves and copy creates a new Team. t1.members eq t2.members is false because each Array("x","y") call allocates a separate array object.

Consider the following Scala 2.13 code. The collection .view creates a lazy SeqView whose map, filter, and take operations are fused and deferred until a strict operation (.toVector) forces traversal. Determine the exact output printed to the console.#

import scala.collection.mutable.ArrayBuffer

val log = ArrayBuffer.empty[String]
def track(label: String)(n: Int): Int = { log += label; n }

val result = (1 to 6).view
  .map(track("M"))
  .filter(_ % 2 == 0)
  .map(track("M2"))
  .take(2)
  .toVector

println(result.mkString(","))
println(log.mkString(","))
Show answer
2,4
M,M,M2,M,M,M2
Why:

On a SeqView, map, filter, and take compose into a single lazy pipeline; elements are pulled through the entire chain one at a time when .toVector forces evaluation. take(2) needs two even numbers, so traversal stops as soon as the second one is produced. Element 1: track("M") fires (log=[M]), 1 is odd so it is discarded. Element 2: track("M") fires (log=[M,M]), 2 passes the filter, then track("M2") fires (log=[M,M,M2]) — that is the first element collected. Element 3: track("M") fires (log=[M,M,M2,M]), 3 is odd, discarded. Element 4: track("M") fires (log=[M,M,M2,M,M]), 4 passes the filter, then track("M2") fires (log=[M,M,M2,M,M,M2]) — second element collected, so take(2) is satisfied and traversal halts. The resulting vector is Vector(2, 4) and the log is M,M,M2,M,M,M2. The interleaving of M and M2 entries is the hallmark of lazy fusion: unlike a strict pipeline (which would run all map("M") calls first), the view pulls each element through every stage before advancing to the next source element.

In Scala, how can a companion object access private members of its companion class (and vice versa) when the JVM itself would forbid it, and which modifier prevents even companion access?#

Show answer

The Scala compiler permits mutual access to private members between a class and its companion object defined in the same source file by emitting synthetic accessor methods (typically with package- or public-level JVM visibility) that bridge the gap — the access control is enforced at the source level, not the bytecode level. However, private[this] (object-private access) restricts visibility to the current instance only, so even the companion object cannot reach those members; no synthetic accessor is generated for private[this] fields.

Why:

Scala's private is a source-level concept: the compiler validates it during type checking but, for companions, generates synthetic accessors so the JVM allows the call. private[this] is stricter — it forbids access from any other instance, including the companion, and no accessor is emitted.

In Scala trait linearization, how is a super call inside a trait method resolved, and why does this mechanism enable stackable modifications?#

Show answer

super in a trait is not statically bound to the trait's declared parent; it is resolved dynamically at runtime against the class's linearization order. The linearization lists traits right-to-left (last mixed in first), then the base class. When a trait method calls super.method(), dispatch proceeds to the next type to the left in the linearization — the next trait in the stack, then eventually the class. Because each trait's super call delegates to the layer below it, mixins compose as a chain of wrappers: each can perform work before and/or after delegating, so you can stack behaviors like logging, caching, and filtering in a single extends/with clause and control their composition order purely by the mixin sequence.

Why:

Trait super is late-bound to the linearization, not the static supertype. This lets each mixed-in trait act as a decorator around the rest of the stack, which is the foundation of the stackable modifications pattern in Scala.

In Scala 2.13 and Scala 3, given:#

Options

Show answer

Yes, in Scala 2.13 and Scala 3 the statement holds. == on a case class with an Array[Int] field uses reference equality (JVM Object.equals), so two distinct arrays with the same contents are not equal. Meanwhile the generated hashCode uses java.util.Arrays.hashCode for array fields (content-based), so two Wrapper instances wrapping arrays with identical elements share the same hash code. This does not violate the equals/hashCode contract.

Why:

The generated equals for a case class compares each field using ==. For Array[Int], Scala == delegates to the JVM array's equals, which is Object.equals (reference equality), so two distinct array instances with identical contents are not equal — Wrapper(Array(1, 2)) == Wrapper(Array(1, 2)) is false. The generated hashCode in Scala 2.13 and Scala 3 uses content-based hashing for array fields: in Scala 2.13, ScalaRunTime._hashCode special-cases arrays via productElementHashCode, which calls java.util.Arrays.hashCode; in Scala 3, Statics.anyHash likewise dispatches to java.util.Arrays.hashCode for arrays. Two arrays with the same elements therefore yield the same hash contribution, so both Wrapper instances produce the same overall hashCode. (Note: in Scala 2.12 and earlier, ScalaRunTime._hashCode did not special-case arrays, so hashCode would be identity-based and the two instances would differ — which is why the version is pinned to 2.13+.) This does not violate the equals/hashCode contract, which requires equal objects to share a hash code, not the reverse.

Related interview questions

Job market

See scala salaries and hiring demand from live job postings.

The other 75 questions

This page shows 25 and marks what you pick. That's as far as a page can go. A free account opens the other 75 and keeps every answer. What you miss comes back until it's right: after a day, then at longer gaps.

Start with this topic

Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan

Interview booked? Practise the job ad instead

What moved, monthly

One email a month when the bulletin comes out: what moved in the markets we track, and the new question topics we published. Confirm your address to join. Unsubscribe any time.