Scala 3 Module 3
Domain modelling: classes, traits, case classes and enums interview questions
Interview questions on “Domain modelling: classes, traits, case classes and enums”: warm-ups, then what each harder question tests and how a good answer starts.
Warm-up questions
Short questions with short answers, each answered in full.
-
Fresher
What is a companion object in Scala, and what is it used for?
Answered in the lesson Classes, objects and companion objects
-
Fresher
What is the difference between a trait and an abstract class in Scala 3?
-
Fresher
What does the compiler generate for a case class?
-
Fresher
What is an algebraic data type, and how do you write one in Scala 3?
-
Fresher
What is an extractor object in Scala?
-
Fresher
What is an opaque type in Scala 3, and how is it different from a type alias?
Questions with model answers
What each question tests, the first step of a good answer and the follow-ups to expect.
-
Fresher to senior
A payments service has a Money class with a public constructor and a validate method that callers are meant to run first. Invalid amounts still reach the database every few weeks. How would you redesign the class so that an invalid Money cannot exist, and what would you check before rolling the change out?
- What it tests
- Private constructors behind a companion factory, Either for expected failures, immutability, and where Scala's private stops protecting a class: Java callers, reflection-based libraries and copies.
- First step
- Say why the bug keeps coming back: a public constructor lets every caller build a Money and forget validate. Make the constructor private and move the checks into a factory in the companion that returns Either[String, Money], so the type tells each caller that the call can fail and why.
- Follow-ups
-
- Why return Either from the factory instead of throwing an exception?
- Java code and a JSON library also create Money values. Does the private constructor stop them?
- How would you move a code base that calls new Money(…) in hundreds of places?
From the lesson Classes, objects and companion objects
Model answer, Pro. Model answers open with any pass; prices are in the notice on this page.Opening the model answers with your pass…
Model answers open with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so the model answers can’t be opened at the moment. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
Your team publishes a library with a plain class RateCalculator. A client team extended it in their own code and overrode one method. After an internal refactor of yours, their subclass gives wrong totals, with no compile error anywhere. What went wrong, and how would you declare your library's classes from now on?
- What it tests
- The fragile base class problem, the extension contract that open documents, final for classes not designed for subclassing, ad hoc extensions and how to find them with -source future, and composition as the alternative.
- First step
- Explain the mechanism: the refactor changed which of the class's own methods call the overridden one, so the client's override now runs at another moment or not at all. Nothing in the declaration said the class was designed for subclassing, because it was neither open nor final.
- Follow-ups
-
- Which classes would you make final, and which open?
- How can the client get the behaviour they wanted without a subclass?
- How do you discover who extends your classes ad hoc today?
From the lesson Classes, objects and companion objects
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
A checkout class mixes in two traits, FraudCheck and Audit, that both override process and call super.process. In a clean-up, a teammate swapped their order in the extends clause, and audit records are now written before the fraud check runs. Explain what changed, and how you would make the order hard to get wrong.
- What it tests
- Linearization and the order of stacked super calls, why reordering mixins changes behaviour without any compile error, and safer designs: tests of the order, explicit composition, one trait that fixes the sequence.
- First step
- Write out the linearization before and after the swap: the last trait written runs first, and each super call moves one step to the left. Swapping the two traits swapped which wrapper starts first and which finishes last, and the compiler had no reason to complain.
- Follow-ups
-
- How would you test that the order is right?
- What would you use instead of stacked traits for a fixed pipeline of steps?
- When are stackable traits still a good fit?
From the lesson Traits, mixins and linearization
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to senior
A shared trait computes val description = s"limit $limit" from an abstract val limit that each account class defines. In production the logs show limit 0 for every kind of account, although each class sets its own limit. Why does this happen, how do you fix it, and how do you stop the same bug elsewhere?
- What it tests
- The order in which traits and classes are set up, JVM zero values, trait parameters, lazy val and def as fixes, and the -Wsafe-init checker in CI.
- First step
- Explain the order of set-up: the trait's body runs before the class body, so description is computed while limit still holds the zero value, and as a val it never changes. Then pick a fix that either passes limit in before the trait runs or computes description after the class is set up.
- Follow-ups
-
- Which fix would you choose, a trait parameter, a lazy val or a def, and why?
- Why did the compiler not warn by default?
- How do you find the other places with the same bug?
From the lesson Traits, mixins and linearization
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to senior
An order service puts Order case classes into a Set to drop duplicates before it writes a daily report, but the same order still appears two or three times. Order has a field tags: Array[String]. Explain why the Set keeps the duplicates, how you would fix it, and what else this field type puts at risk.
- What it tests
- How the generated equals and hashCode depend on each field's own equality, why arrays compare by reference, and choosing immutable collections for data types.
- First step
- Follow the generated equals: it compares every field with that field's own equals, and an Array's equals is reference equality. Two orders read separately hold two different arrays, so they are never equal and their hash codes differ, and the Set keeps both.
- Follow-ups
-
- Why is changing the field to a List or Vector enough?
- What can go wrong when the array inside an Order is changed after the order was stored?
- What if the type comes from a library you cannot change?
From the lesson Case classes and immutable data
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
case class Discount(percent: Int) has a companion apply that rejects values above 100, yet discounts of 150 % keep reaching the database. How can such values be built, and how do you make the check impossible to skip in Scala 3?
- What it tests
- The generated methods that build a case-class value without apply, private constructors closing copy and apply in Scala 3, factories that return Either, and the paths outside the Scala compiler.
- First step
- List every way a Discount can be built: the custom apply checks its argument, but new Discount(150) calls the public constructor directly, and copy(percent = 150) on any valid Discount calls it too. The check guards one door out of three.
- Follow-ups
-
- What changes when the constructor is made private?
- Should the factory throw, or return Either?
- Which ways in remain even with a private constructor?
From the lesson Case classes and immutable data
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to senior
A teammate wants to store each payment's status as Status.ordinal in a small integer column, to save space. What could go wrong over the next year, what would you store instead, and how would you move rows that already hold ordinals?
- What it tests
- What an ordinal is, why it changes when cases are added or reordered, stable encodings for enums, and a safe migration of stored data.
- First step
- Say what an ordinal is: the position of the case in the source, counted from 0. It is stable only while nobody inserts, removes or reorders a case, and the database would silently reinterpret every stored number the day someone does.
- Follow-ups
-
- Why not store the name with toString and read it back with valueOf?
- How would you migrate the rows that already store ordinals?
- How do you stop the code and the stored values from drifting apart again?
From the lesson Enums, sealed traits and ADTs
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
You model an order's events: placed, paid and shipped, plus three kinds of failure that share a retry rule, some carrying fields of their own. A Java service reads the events too. Would you use an enum or a sealed trait hierarchy, and how would you defend the choice in a design review?
- What it tests
- What enum cases can and cannot hold, grouping cases under a sealed trait, exhaustivity across a hierarchy, and what each encoding looks like from Java.
- First step
- List what the model needs: cases with different fields, a group of failures with a shared supertype and member, and checked matches. Enum cases cannot have bodies or form groups, so the failures point to a sealed hierarchy; then check what the Java service needs.
- Follow-ups
-
- What would the enum version look like, and where does it break?
- How does the Java service match on the events?
- How do you keep matches over the hierarchy complete as new events arrive?
From the lesson Enums, sealed traits and ADTs
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to senior
A nightly job reads a partner's CSV file with val Array(user, amount) = line.split(","). It ran for months, then stopped with scala.MatchError on a line with a note in a third column, and no line after it was processed. Why did the compiler not warn, how do you fix the job, and how do you find similar code?
- What it tests
- Refutable patterns in val definitions, why an Array pattern escapes the warning, matching with a case per shape, error values instead of exceptions for bad input, and finding the pattern in a code base.
- First step
- Name the mechanism: a pattern in a val is a match with one case, so any line that does not have exactly two fields throws MatchError. The compiler warns only when the pattern's type is narrower than the value's, as for first :: rest; Array(user, amount) has the array's own type, so it drew no warning and nothing told the authors the line could fail.
- Follow-ups
-
- Would adding .runtimeChecked have helped?
- What should the job do with a bad line?
- How do you find other vals with patterns that can fail?
From the lesson Scala 3 pattern matching in depth
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 4 minutes
-
Mid to senior
A consumer receives messages typed Any and handles batches with case xs: List[Payment] => total(xs). Now and then a ClassCastException is thrown inside total, far from the match, and only for some messages. Explain why the match let the batch through, and how you would make the handling safe.
- What it tests
- Type erasure and unchecked type tests, reading warning E092, testing elements instead of type arguments, ClassTag or TypeTest, and decoding messages into a typed model at the boundary.
- First step
- Explain what the test can check while the program runs: only that the value is a List, because the element type is erased. A batch of other objects passes the case, and the cast fails later, when total reads an element as a Payment. The compiler said so with E092.
- Follow-ups
-
- What would you change in the match itself?
- Where should the types be decided instead?
- How do you stop such matches from creeping back in?
From the lesson Scala 3 pattern matching in depth
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Fresher to senior
A billing service keeps amounts as Long, some in paise and some in rupees, and a refund was paid out 100 times too large. How would you make this mix-up impossible to compile? Compare a case class wrapper, a value class and an opaque type, and say what each costs at run time.
- What it tests
- Using the type system to separate units, the run-time cost of three kinds of wrapper on the JVM, smart constructors, and converting at the edges of the system.
- First step
- Name the root cause: two units share one type, so the compiler cannot tell them apart. Give each unit a type of its own, built only through a factory, with conversions written once, and then choose the wrapper by what it costs where the amounts are stored and passed.
- Follow-ups
-
- Which wrapper would you pick for amounts that sit in large arrays, and why?
- Where do conversions between paise and rupees belong?
- How do you introduce the new type without changing every file at once?
From the lesson Opaque types and value classes
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes
-
Mid to senior
An object had formatPaise(p: Long) and formatGrams(g: Long), which a Java module calls. A teammate made Paise and Grams opaque types over Long and renamed both methods to format, one taking a Paise and one a Grams. The build now fails with E120. Explain the failure, fix it, and say what the Java callers see.
- What it tests
- Erasure of opaque types to their underlying type, how overloads become JVM methods, @targetName and the JVM names it sets, and keeping Java callers working through a refactoring.
- First step
- Start from erasure: the type checker sees format(Paise) and format(Grams), but on the JVM both parameters are long, so both methods would be format(long), and one class cannot hold two methods with the same signature.
- Follow-ups
-
- Why did the old version, with two differently named methods, have no such problem?
- How can the Java callers keep working unchanged?
- Would you keep the overloads or go back to two names?
From the lesson Opaque types and value classes
Model answer, Pro.In the answer:
- Model answer
- Follow-up answers
- Common mistakes
- Rubric
- Variants
- about 3 minutes