Articles
Scala Interview Questions: Collections, Pattern Matching, and Futures
Practice Scala interview questions on collections, pattern matching, Futures, error handling, and concurrency with clear traces and worked examples.
Scala interview questions often look like short syntax puzzles, but the useful answers explain collection shape, evaluation order, and failure propagation. A compact expression can allocate more than expected, skip a branch, or start asynchronous work earlier than its indentation suggests.
The direct answer: choose collections from their operations, trace map and flatMap using concrete types, make pattern matches cover the data model, and explain when each Future is created. A correct result without that reasoning leaves important behavior unexplained.
These original exercises focus on Scala language and library semantics, not Spark execution. Examples use syntax familiar to Scala 2.13 and Scala 3 unless stated otherwise.
How do you choose between List, Vector, and mutable storage?
Begin with the operations the program performs. A List is well suited to head access and prepending, while indexed access traverses the list. Scala's collections performance reference describes Vector as offering effectively constant access for several common operations. Neither observation means every workload has the same allocation or cache behavior.
Suppose an exercise reads 100,000 measurements and repeatedly accesses the middle element. A linked list is a poor fit even if its construction was convenient. If the exercise instead recursively consumes the head and tail, a list fits the algorithm naturally.
For a builder that accumulates results, mutable storage can be appropriate inside a bounded scope before returning an immutable result. Explain who can see the mutation. "Functional code never mutates" is less useful than an ownership argument that prevents partially built state from escaping.
Finally, separate asymptotic claims from benchmarks. State the operation's complexity, then measure the actual input distribution and implementation if latency or allocation matters.
What changes when you replace map with flatMap?
Use a concrete type transformation instead of saying flatMap "flattens everything." Starting from List(1,2,3), map each number to a two-element list containing the number and ten times that number.
val input = List(1, 2, 3)
val nested = input.map(n => List(n, n * 10))
val flat = input.flatMap(n => List(n, n * 10))
The first result is List(List(1,10), List(2,20), List(3,30)), with type List[List[Int]]. The second is List(1,10,2,20,3,30), with type List[Int]. The function still produces a collection for each input; flatMap combines that returned layer into the outer result.
Now change the function so even inputs return an empty list. The flat result becomes List(1,10,3,30). This demonstrates why flatMap can change cardinality: one input can contribute zero, one, or several outputs.
For an interview follow-up, ask whether order is preserved for the chosen collection and whether the transformation has side effects. Do not silently generalize a list trace to every possible container.
How do Option and Either change the same reasoning?
Option[A] represents an optional value. Mapping an Option[Int] with a function returning Option[String] produces a nested option; flatMap expresses a dependent step that may also be absent. Either can carry a failure value rather than reducing every problem to absence.
Consider a request with an optional account identifier. Parsing the identifier can fail; looking up a valid identifier can return no account; authorization can reject an existing account. Treating all three as None loses information that the caller may need.
A reasonable design uses a domain error type for invalid input and forbidden access, reserving optionality for a genuinely optional result. The exact model depends on the API contract, not on a rule that one container is always superior.
In a worked answer, name the type after each step. If the result unexpectedly becomes Option[Option[Account]], explain whether those two layers encode two distinct meanings or whether flatMap should remove accidental nesting. Types are evidence about the contract, not just compiler obstacles.
When does a collection view execute its work?
Scala's view documentation distinguishes deferred transformations from strict collection construction. A view can avoid intermediate collections, but it also changes when transformation functions execute. Converting a view to a strict collection forces the necessary work.
Imagine a transformation increments a diagnostic counter. On a strict list, mapping runs while producing the mapped list. On a view, creating the transformation pipeline does not necessarily perform the same work immediately; traversal triggers evaluation. Repeating traversal can repeat computation and side effects.
This is why a view should not be described as a cache. If a calculation must run once and its result be reused, materialize the result deliberately. If only the first few transformed elements are needed, deferred evaluation may avoid unnecessary work.
An iterator adds another concern: consumption. Passing the same iterator to two consumers does not give each a fresh replay of the original sequence. Ask whether the API needs reusable data, a single pass, or an explicitly memoized lazy structure.
How do you make a pattern match complete?
Pattern matching should expose the alternatives in your data model. An option has present and absent cases; a sealed hierarchy can represent a known family of domain alternatives. Guards refine a case but do not automatically cover everything else.
def classify(value: Option[Int]): String = value match {
case Some(n) if n > 0 => "positive"
case Some(_) => "nonpositive"
case None => "absent"
}
Here Some(4) produces positive, Some(0) produces nonpositive, and None produces absent. Removing the second case leaves zero and negative integers uncovered. The first guard does not turn every Some into a handled value.
In domain code, a wildcard fallback can be convenient but can also hide a newly added case that deserves explicit behavior. Discuss whether a default is semantically valid. A payment state machine, for example, should not silently interpret a new review state as successful merely because it matches a catch-all branch.
What does a for-comprehension promise?
A for-comprehension expresses combinations of operations such as map, flatMap, and filtering through the receiver's methods. Its meaning depends on the involved types. Similar syntax over a list, option, or future does not imply identical execution behavior.
For two lists, a comprehension can enumerate combinations. Two lists of lengths two and three can yield six pairs. For an option, an absent intermediate value can stop the successful path. For a future, the expression composes asynchronously completed results.
Trace one step at a time: what value enters the next generator, what type wraps it, and what happens if the previous step cannot produce a successful value? That explanation is more robust than memorizing a mechanical rewrite without understanding its receiver.
Also inspect guards. A guard in asynchronous code can turn a successful completion into a failed future when its predicate is not satisfied. If rejection is a normal business outcome, an explicit result type can communicate intent more clearly than relying on a filtering failure.
When does a Future begin running?
Scala's standard Future represents a result that may complete later. Creating one with Future { ... } submits work through an ExecutionContext; the body is not a reusable lazy recipe that starts only when someone awaits it.
For two independent requests, suppose A takes 80 milliseconds and B takes 120 milliseconds. If both are created before their results are composed, an idealized elapsed time is 120 milliseconds. If B is created only after A completes, the idealized total is 200 milliseconds. These numbers ignore scheduling, contention, and network overhead.
The lesson is about creation, not a performance guarantee. A comprehension that refers to two already-created futures does not make their work sequential. A comprehension that creates the second inside the first dependency does make its creation depend on the first result.
If B needs an identifier returned by A, that dependency is legitimate. If B is independent, accidental sequential creation wastes an opportunity for overlap. Show the dependency graph before rewriting the syntax.
Which operations change a Future's success or failure?
Different combinators answer different questions. Mapping transforms a successful result; flatMap chains another asynchronous result. Recovery supplies an alternative for selected failures. Observing completion is different from producing a new business result.
| Intent | Typical operation | Question to ask |
|---|---|---|
| Convert a successful value | map | Is the function synchronous? |
| Start a dependent asynchronous step | flatMap | Does it return another Future? |
| Supply a fallback value | recover | Which failures are acceptable to replace? |
| Supply asynchronous fallback work | recoverWith | Can the fallback also fail? |
For an account service, recovering every exception as an empty account list can disguise an outage as "this customer has no accounts." Restrict recovery to the failures that the product contract actually permits, and preserve other failures for the caller.
Avoid placing correctness-critical sequencing in unrelated callbacks. Completion callbacks are useful for observation, but composing values makes dependencies visible in the returned future. A log line that happened to appear first in one run is not an ordering contract.
What does the ExecutionContext decide?
An ExecutionContext controls where asynchronous tasks are scheduled. It does not make blocking work nonblocking. A task waiting on a synchronous database call still occupies execution resources while it waits.
Consider a service that accepts 200 simultaneous requests, each making a blocking call lasting one second. Moving those calls into futures changes how the caller receives results, but it does not create unlimited database connections or worker capacity. Identify the queue and resource limit before choosing concurrency.
A dedicated, bounded strategy for blocking work can isolate it from unrelated computation. Size it with the connection pool and downstream capacity in mind. Marking a region as blocking can inform an execution context, but compensation behavior depends on its implementation.
In an interview, resist promising a specific thread count from the syntax alone. Describe which execution context the code receives, what type of work it runs, and how overload is bounded. Then explain how you would observe saturation and latency.
Does a timeout cancel the underlying work?
A timeout on waiting for a result is not automatically cancellation of the computation producing it. Standard Scala futures do not provide a general built-in cancellation protocol for arbitrary work. The underlying client or operation needs its own supported cancellation or deadline behavior.
Suppose the caller gives up after 50 milliseconds but an HTTP request continues for another second. Retrying immediately can create two active requests. If the operation charges a card, the retry policy requires a stable idempotency key and a clear response contract.
Likewise, retrying an already-created failed future just observes the same failure. To repeat work, a retry mechanism needs a way to create a new attempt, subject to bounded attempts, delay, and the operation's retry safety.
For a practice answer, distinguish caller deadline, transport timeout, server execution, and cancellation acknowledgement. Those are separate events. A UI that stops showing a spinner has not proved that a remote side effect stopped.
How would you review a small Scala service?
Use a fictional inventory endpoint that loads two independent sources, combines quantities, and returns a typed result. First identify which calls can start together. Then decide what missing inventory means, which failures should propagate, and whether partial data is allowed.
Next inspect collection work. A repeated indexed lookup into a list may dominate CPU time; a lazy view with logging may repeat side effects; a nested option may conceal an accidental type layer. Keep each diagnosis tied to one observable behavior.
Test a normal response, an absent item, a failure in each source, and a delayed completion. Assert returned values and failures rather than callback print order. For concurrency tests, prefer controllable promises or test doubles over fragile assumptions about sleep durations.
Finally, explain the result in ninety seconds: the data shape, the dependency between computations, the failure contract, and the bounded resource. That is a stronger preparation exercise than collecting isolated definitions of functional-programming vocabulary.
Sources and Further Reading
- Scala collection performance characteristics describes operation costs for Scala 2.13 collections.
- Scala views explains deferred collection transformations.
- Scala pattern matching covers cases, guards, and domain examples.
- Scala for-comprehensions introduces generator and yield behavior.
- Scala Futures and Promises documents execution contexts, composition, callbacks, blocking, and failure handling.
Sources reviewed September 24, 2026. Verify compiler warnings, collection APIs, and execution-context behavior against the project's Scala version; the timing scenarios are illustrative, not benchmarks.