Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Scala 3  Module 3 – Domain modelling: classes, traits, case classes and enums

Classes, objects and companion objects

Write Scala 3 classes with parameters, defaults and access modifiers, build them without new, and use singleton and companion objects for factories.

  • Beginner
  • 20 minutes
  • Examples run with scala 3.9.0, Java 25.0.4.1 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Define classes with constructor parameters, default values, auxiliary constructors and access modifiers
  • Use singleton and companion objects for shared members, factories and private access
  • Write a validating factory that returns Either, so that no invalid instance can be built
  • Decide whether a class should be open, final or neither, and predict what the compiler reports

Before you start

What you’ll learn · Before you start

What you will learn

  • Define classes with constructor parameters, default values, auxiliary constructors and access modifiers
  • Use singleton and companion objects for shared members, factories and private access
  • Write a validating factory that returns Either, so that no invalid instance can be built
  • Decide whether a class should be open, final or neither, and predict what the compiler reports

Before you start

On this page

If anyone can write new Account("A1", -500), then sooner or later an account holds a bad id and a negative balance, and every method that touches it has to check again. In this lesson’s runs, a private constructor and one factory method in a companion object made Scala 3.9 refuse both ways around the check, Account("A-1", 0) and new Account("A-2", 0), at compile time.

You will write classes with constructor parameters, defaults, auxiliary constructors and access modifiers, build them without new, use singleton objects for shared members and companion objects for factories, and decide whether a class should be open, final or neither. Every behaviour is shown by a program that the compiler built and ran.

A class bundles parameters, fields and a body

An invoice needs a customer, an amount in paise and a tax rate that is usually 18 %. This class takes them as constructor parameters, works out the tax in its body and offers the total as a method:

invoice/Invoice.scala Scala · invoice/Invoice.scala
class Invoice(val customer: String, val paise: Long, taxPercent: Int = 18):
  println(s"building the invoice for $customer")
  val taxPaise: Long = paise * taxPercent / 100

  def this(customer: String) = this(customer, 0)

  def total: Long = paise + taxPaise

@main def invoices(): Unit =
  val a = Invoice("Asha", 120_000)
  println(s"${a.customer}: ${a.total} paise, ${a.taxPaise} of it tax")
  val b = new Invoice("Ravi", 50_000, taxPercent = 5)
  println(s"${b.customer}: ${b.total} paise")
  val c = Invoice("Meera")
  println(s"${c.customer}: ${c.total} paise")
auxiliary/AuxFirst.scala, an auxiliary constructor that prints first Scala · auxiliary/AuxFirst.scala
class Invoice(val customer: String, val paise: Long):
  def this(customer: String) =
    println("an invoice without an amount")
    this(customer, 0)
Three invoices, the fields the JVM sees, and a constructor that starts wrong Python · invoice_session.py
from session import run

# A class with val parameters, a plain parameter with a default, a body and an auxiliary constructor;
# the fields the JVM sees, listed by javap; and an auxiliary constructor that does not start with this(…).
run("""
cd invoice
scala run Invoice.scala --server=false
scala compile Invoice.scala --server=false -d out
javap -p -cp out Invoice
cd ../auxiliary
!scala compile AuxFirst.scala --server=false
""")

Output

$ cd invoice
$ scala run Invoice.scala --server=false
building the invoice for Asha
Asha: 141600 paise, 21600 of it tax
building the invoice for Ravi
Ravi: 52500 paise
building the invoice for Meera
Meera: 0 paise
$ scala compile Invoice.scala --server=false -d out
$ javap -p -cp out Invoice
Compiled from "Invoice.scala"
public class Invoice {
  private final java.lang.String customer;
  private final long paise;
  private final long taxPaise;
  public Invoice(java.lang.String, long, int);
  public java.lang.String customer();
  public long paise();
  public long taxPaise();
  public Invoice(java.lang.String);
  public long total();
  public static int $lessinit$greater$default$3();
}
$ cd ../auxiliary
$ scala compile AuxFirst.scala --server=false
-- [E040] Syntax Error: AuxFirst.scala:3:4
3 |    println("an invoice without an amount")
  |    ^^^^^^^
  |    'this' expected, but identifier found
-- [E050] Type Error: AuxFirst.scala:4:4
4 |    this(customer, 0)
  |    ^^^^
  |    class Invoice does not take parameters
  |
  | longer explanation available when compiling with `-explain`
2 errors found
Compilation failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 invoice_session.py

The parameter list after the class name is the primary constructor: The constructor made of a class's parameter list and its body. Every instance is built by it: val parameters become fields, and the statements of the body run from top to bottom.. A parameter marked val becomes a field that anyone can read, like customer and paise; var would make it writable too. A plain parameter such as taxPercent is only an input to the constructor. It has a default, 18, so the first invoice left it out, and the second set it by name, taxPercent = 5.

The class body runs as part of the constructor, once for every instance: three invoices printed three lines that say building the invoice for …. def this(customer: String) = this(customer, 0) is an auxiliary constructor: An extra constructor written def this(…) in a class body. Its first statement must call another constructor of the class, so every instance still passes through the primary constructor.. Its first statement must call another constructor of the class, so every instance still passes through the body. The last build of the session tries AuxFirst.scala, whose auxiliary constructor prints first, and the compiler stops at the println: 'this' expected, but identifier found. A default value usually does the same job with less code.

Only b was built with new. Scala 3 generates a universal apply method: An apply method that Scala 3 generates for each constructor of a concrete class, so Invoice("Asha", 120_000) builds an instance without new. None is generated when the companion already has a member named apply. for each constructor of a class, so Invoice("Asha", 120_000) and Invoice("Meera") call those. Step through the first invoice:

Interactive · message by message How Invoice("Asha", 120_000) builds an instance Step through the first invoice of Invoice.scala, from the call without new to the first use of a method.
How Invoice("Asha", 120_000) builds an instance, step 6 of 6. Messages so far: invoices() to Invoice.apply: Invoice("Asha", 120_000); Invoice.apply to the new Invoice: new Invoice(…, 18); the new Invoice to invoices(): the new Invoice; invoices() to the new Invoice: a.total.invoices()the @main methodInvoice.applygenerated by the compilerthe new Invoicetotal gives 141600Invoice("Asha", 120_000)new Invoice(…, 18)the new Invoicea.total

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A call without new. invoices() writes Invoice("Asha", 120_000) with no new. Scala 3 generated an apply method for each constructor of Invoice, so this call goes to apply.
  2. The default fills in. The call gives two arguments, so the third parameter, taxPercent, takes its default, 18. apply then calls the primary constructor with all three.
  3. The body runs. The class body is part of the constructor and runs from top to bottom, so its println prints building the invoice for Asha before anything else happens.
  4. A field is worked out. taxPaise = 120_000 * 18 / 100 is computed once, now, and kept as a field of this instance, 21,600 paise.
  5. The instance is ready. The constructor hands back the new Invoice. customer, paise and taxPaise are fields with accessors. taxPercent was only needed while building, so the class keeps no field for it.
  6. A method runs on each call. a.total is a def, so its body runs every time it is called, 120,000 + 21,600 = 141,600 paise, as the program printed.

The javap listing at the end of the session shows what the compiler kept: fields for customer, paise and taxPaise, each with an accessor method, and nothing for taxPercent, because only the body used it. The default 18 became a small static method that a call without the argument runs.

Checkpoint

Check: Constructor parameters, fields and the class body

  1. Question 1 of 2 A program builds four invoices with Invoice(…) and then calls total twice on each. How many lines that start with building the invoice does it print?

    Type a number.

    A hint

    Which code runs when an instance is built, and which runs when a method is called?

    Show the answer: Question 1 of 2

    Answer: 4 lines

    The println is in the class body, which runs once for every instance: four invoices, four lines. total is a def without a println, so the eight calls print nothing.

  2. Question 2 of 2 Invoice("Meera") compiled although Invoice is a plain class and the call has no new. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Scala 3 generates an apply method for each constructor of a concrete class

    The Reference calls them constructor proxies: a companion with one apply per constructor, generated while the class has no companion apply of its own.

Access modifiers decide who can read a member

A wallet keeps its balance to itself and offers an audit line only to its subclasses. This program tries to read three members from outside the class, and the compiler refuses all three:

access/Access.scala Scala · access/Access.scala
class Wallet(val owner: String, private var balancePaise: Long, limitPaise: Long = 1_000_000):
  def add(paise: Long): Unit =
    if balancePaise + paise <= limitPaise then balancePaise += paise
  protected def audit: String = s"$owner holds $balancePaise paise"

class FamilyWallet(owner: String, start: Long) extends Wallet(owner, start):
  def report: String = "family wallet: " + audit

@main def access(): Unit =
  val w = Wallet("Asha", 500)
  println(w.owner)
  println(w.limitPaise)
  println(w.balancePaise)
  println(w.audit)
  println(FamilyWallet("Ravi", 700).report)
Three members that main may not read Python · access_session.py
from session import run

# Reading a plain parameter, a private field and a protected method from outside the class.
run("""
cd access
!scala compile Access.scala --server=false
""")

Output

$ cd access
$ scala compile Access.scala --server=false
-- [E173] Reference Error: Access.scala:12:12
12 |  println(w.limitPaise)
   |          ^^^^^^^^^^^^
   |value limitPaise cannot be accessed as a member of (w : Wallet) from the top-level definitions in package <empty>.
   |  private value limitPaise can only be accessed from class Wallet.
-- [E173] Reference Error: Access.scala:13:12
13 |  println(w.balancePaise)
   |          ^^^^^^^^^^^^^^
   |variable balancePaise cannot be accessed as a member of (w : Wallet) from the top-level definitions in package <empty>.
   |  private variable balancePaise can only be accessed from class Wallet.
-- [E173] Reference Error: Access.scala:14:12
14 |  println(w.audit)
   |          ^^^^^^^
   |method audit cannot be accessed as a member of (w : Wallet) from the top-level definitions in package <empty>.
   |  protected method audit can only be accessed from class Wallet or one of its subclasses.
3 errors found
Compilation failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 access_session.py

The errors name the three rules. limitPaise is a plain parameter that the method add uses, so the compiler keeps it as a private field: private value limitPaise can only be accessed from class Wallet. balancePaise is declared private var. audit is protected, which the message spells out: it can only be accessed from class Wallet or one of its subclasses, which is why FamilyWallet.report drew no error.

Modifier Who may read the member
none (public) All code
protected The class, its companion and its subclasses, but not the rest of the package
private[billing] All code inside package billing and its subpackages; nothing outside, not even a subclass
private The class and its companion only

The table holds two surprises for Java programmers. protected does not open a member to the rest of its package, as it does in Java. And a companion object, which comes two sections on, sees the private members of its class. private[billing] is the qualified private: private[pkg] on a member: it is visible everywhere inside package pkg and its subpackages, and nowhere else. It sits between private and public. More in “Packages, imports, exports and top-level code” of the lesson on packages.

Writing private[this] in new code

Older Scala code marks fields private[this]. The Scala 3 Reference deprecates private[this] and protected[this]: write private, and the compiler itself works out which private members are only used through this.

Checkpoint

Check: private, protected and qualified access

  1. Question 1 of 2 A Java programmer writes class Audit in the same package as Wallet and calls w.audit, which Wallet declares protected. What does Scala do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It refuses the call, because protected members are open only to the class, its companion and its subclasses

    The message on w.audit read protected method audit can only be accessed from class Wallet or one of its subclasses. Use private[pkg] when a member should be visible in a package.

  2. Question 2 of 2 Given class Rate(val name: String, percent: Int), which members can code outside the class read?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: name only

    Plain parameters are inputs to the constructor. When a method uses one, the compiler keeps it as a private field, but it is still not reachable from outside.

An object is one instance, set up on first use

Some values belong to no single invoice: the tax rates themselves. An object: A singleton: a class with exactly one instance, created when it is first used. The members of an object play the role of Java's static members. More in “Scala 3 for Java and Python developers” holds them. It is a class with exactly one instance, which the program never creates with new:

rates/Rates.scala Scala · rates/Rates.scala
object TaxRates:
  println("TaxRates: setting up the rates")
  val standard: Int = 18
  val reduced: Int = 5

@main def rates(): Unit =
  println("main: started")
  println(s"standard rate: ${TaxRates.standard} %")
  println(s"reduced rate: ${TaxRates.reduced} %")
The order in which the lines appear Python · rates_session.py
from session import run

# When an object's body runs: at the first use of the object, and only once.
run("""
cd rates
scala run Rates.scala --server=false
""")

Output

$ cd rates
$ scala run Rates.scala --server=false
main: started
TaxRates: setting up the rates
standard rate: 18 %
reduced rate: 5 %

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rates_session.py

main: started came first, although TaxRates is defined above rates(). Its body ran only when rates() first read TaxRates.standard, and it ran once: the second read, TaxRates.reduced, printed no second set-up line. The Scala 3 Book describes objects this way, set up lazily when their members are first used, as a lazy val is. Step through the four lines:

Interactive · message by message When the body of object TaxRates runs Step through rates() and watch which line makes the object's body run, and which ones do not.
When the body of object TaxRates runs, step 5 of 5. Messages so far: rates() to object TaxRates: TaxRates.standard; object TaxRates to rates(): 18; rates() to object TaxRates: TaxRates.reduced.rates()the @main methodobject TaxRatesstandard 18, reduced 5TaxRates.standard18TaxRates.reduced

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. main starts. rates() prints "main: started". Nothing has used TaxRates yet, so its body has not run and its vals hold nothing.
  2. The first use. The next line reads TaxRates.standard. This is the first use of the object, so the JVM sets it up now.
  3. The body runs once. Setting up runs the object's body from top to bottom, like a class body. Its println prints, and standard and reduced get their values, 18 and 5.
  4. The value comes back. standard is 18, so main prints standard rate 18 %. The object now exists for the rest of the run.
  5. A second use. TaxRates.reduced uses the same single instance. Its body does not run again, which is why setting up the rates was printed only once.

An object can hold what a class body holds, such as vals and defs, and code reaches them as TaxRates.standard or imports them. That makes objects the place for what Java writes as static members.

Key idea

A class body runs once for every instance you create; an object’s body runs once in the whole program, on the first use of the object.

Checkpoint

Check: Objects set up once, on first use

  1. Question 1 of 2 A program reads TaxRates.standard three times and TaxRates.reduced twice. How many times does it print TaxRates: setting up the rates?

    Type a number.

    A hint

    An object has one instance in the whole program.

    Show the answer: Question 1 of 2

    Answer: 1 times

    The object's body runs once, at the first use; the other four reads use the instance that already exists.

  2. Question 2 of 2 Rates.scala defines object TaxRates above rates(). Which line does the program print first?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: main: started

    The session printed main: started, then the set-up line, then the two rates: objects are set up lazily, on first use, as a lazy val is.

A companion object shares private members with its class

An object with the same name as a class, in the same file, is its companion object: An object with the same name as a class, defined in the same file. The two can read each other's private members, and the object holds what belongs to the type rather than to one instance, such as factory methods.. The Scala 3 Book states the rule that makes the pair useful: a companion class or object can access the private members of its companion. Account.scala uses it in both directions:

accounts/Account.scala Scala · accounts/Account.scala
class Account private (val id: String, private val paise: Long):
  def deposit(amount: Long): Either[String, Account] =
    if amount < Account.MinDepositPaise then Left(s"a deposit must be at least ${Account.MinDepositPaise} paise, not $amount")
    else Right(new Account(id, paise + amount))
  override def toString = s"Account($id, $paise paise)"

object Account:
  private val MinDepositPaise = 100
  private val IdFormat = "A-[0-9]{1,6}".r
  private var opened = 0

  def create(id: String): Either[String, Account] =
    if !IdFormat.matches(id) then Left(s"bad account id: $id")
    else
      opened += 1
      Right(new Account(id, 0))

  def richer(a: Account, b: Account): Account = if a.paise >= b.paise then a else b
  def openedSoFar: Int = opened
accounts/Main.scala Scala · accounts/Main.scala
@main def accounts(): Unit =
  println(Account.create("A1"))
  Account.create("A-1") match
    case Left(reason) => println(reason)
    case Right(empty) =>
      println(empty)
      println(empty.deposit(5))
      val topped = empty.deposit(50_000)
      println(topped)
      topped match
        case Right(full) => println(s"richer: ${Account.richer(empty, full)}")
        case Left(reason) => println(reason)
      println(s"still: $empty")
  println(s"accounts opened: ${Account.openedSoFar}")
Two calls of create, two deposits, a comparison, then javap Python · accounts_session.py
from session import run

# A private constructor, a companion with a validating factory, and private members shared both ways;
# then the class as the JVM sees it.
run("""
cd accounts
scala run . --server=false
scala compile Account.scala --server=false -d out
javap -p -cp out Account
""")

Output

$ cd accounts
$ scala run . --server=false
Left(bad account id: A1)
Account(A-1, 0 paise)
Left(a deposit must be at least 100 paise, not 5)
Right(Account(A-1, 50000 paise))
richer: Account(A-1, 50000 paise)
still: Account(A-1, 0 paise)
accounts opened: 1
$ scala compile Account.scala --server=false -d out
$ javap -p -cp out Account
Compiled from "Account.scala"
public class Account {
  private final java.lang.String id;
  private final long paise;
  public Account(java.lang.String, long);
  public java.lang.String id();
  public long Account$$paise();
  public scala.util.Either<java.lang.String, Account> deposit(long);
  public java.lang.String toString();
  public static scala.util.Either<java.lang.String, Account> create(java.lang.String);
  public static int openedSoFar();
  public static Account richer(Account, Account);
}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 accounts_session.py

The class reads its companion’s private MinDepositPaise to refuse the deposit of 5 paise. The companion reads the private field paise of two accounts in richer, and it keeps a private counter, opened, that no single account owns: one account was opened, because the id A1 was refused. Step through who reads what:

Interactive · step by step What class Account and object Account share Step through the accounts program and watch which private members the class and its companion read from each other.
What class Account and object Account share, step 6 of 6. Showing accounts(), object Account (opened is 1), class Account.create(id)new AccountMinDepositPaisedeposit, toStringaccounts()in Main.scalaobject Accountopened is 1class Accountprivate paisedeposit(50_000)

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One name, one file. Account.scala defines class Account and object Account. Because they share a name and a file, each is the companion of the other.
  2. Through the factory. accounts() cannot call the constructor, which is private. It calls Account.create("A-1"), a public method of the companion.
  3. The companion builds it. create checks the id against its private IdFormat, adds 1 to its private counter opened and calls new Account. The private constructor is open to the companion.
  4. The class reads the companion. deposit(5) compares the amount with Account.MinDepositPaise, a private val of the companion. The class may read it, and refuses the deposit of 5.
  5. The companion reads the class. richer(empty, full) compares a.paise with b.paise. paise is private to the class, yet the companion reads it from both accounts.
  6. Everyone else. accounts() uses only public members, create, richer and openedSoFar of the companion, deposit and toString of the class. paise, IdFormat and the constructor are out of its reach.

A companion is also where a class’s own apply methods go. Scala 3 generates the universal apply methods of the first section only while the companion has no member named apply, so writing one replaces them all. Account defines create instead, a name that says the call can be refused.

Checkpoint

Check: Companion objects and shared private members

  1. Question 1 of 2 Account.richer(a, b) reads a.paise, and paise is private to class Account. Why does it compile?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: richer is in object Account, the companion, which may read the private members of its class

    The Scala 3 Book states it: a companion class or object can access the private members of its companion. Main cannot read paise at all.

  2. Question 2 of 2 You give class Invoice(val customer: String, val paise: Long) a companion with def apply(customer: String): Invoice. What happens to a call Invoice("Asha", 120_000)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It stops compiling, because no apply methods are generated once the companion has a member named apply

    Constructor proxies are generated only while the companion has no member named apply. Once you write one, write every apply you want, or use new.

A private constructor makes the factory the only way in

deposit did not change the empty account: still: Account(A-1, 0 paise) came after the deposit of 50,000. Each deposit returns a new Account inside an Either. The API documentation states the convention: Left is used for failure and Right for success, so the type of create, Either[String, Account], tells every caller that the call can fail and why.

The check in create only protects the class if nobody can go around it. This program tries both ways:

bypass/Bypass.scala Scala · bypass/Bypass.scala
class Account private (val id: String, private val paise: Long)

object Account:
  def create(id: String): Either[String, Account] = Right(new Account(id, 0))

@main def bypass(): Unit =
  val a = Account("A-1", 0)
  val b = new Account("A-2", 0)
Two attempts to build an Account without create Python · bypass_session.py
from session import run

# Trying to go around the factory: the generated apply and new both meet the private constructor.
run("""
cd bypass
!scala compile Bypass.scala --server=false
""")

Output

$ cd bypass
$ scala compile Bypass.scala --server=false
-- [E050] Type Error: Bypass.scala:7:10
7 |  val a = Account("A-1", 0)
  |          ^^^^^^^
  |          object Account does not take parameters
  |
  | longer explanation available when compiling with `-explain`
-- [E173] Reference Error: Bypass.scala:8:14
8 |  val b = new Account("A-2", 0)
  |              ^^^^^^^
  |constructor Account cannot be accessed as a member of Account from the top-level definitions in package <empty>.
  |  private constructor Account can only be accessed from class Account.
2 errors found
Compilation failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 bypass_session.py

The word private before the parameter list makes the constructor private. new Account("A-2", 0) failed with E173: the private constructor can only be accessed from class Account, and from its companion, as create shows. Account("A-1", 0) found no apply it could use, so the compiler said object Account does not take parameters. Every Account that Scala code builds has therefore passed the check. Step through the two calls of create:

Interactive · step by step How Account.create refuses a bad id Step through create("A1"), then create("A-1"), and see the only path that builds an Account.
How Account.create refuses a bad id, step 5 of 5. Showing create(id) (id is A-1), id matches A-digits?, Left(reason), opened += 1, new Account(id, 0), Right(account).noyescreate(id)id is A-1id matches A-digits?Left(reason)no Account builtopened += 1new Account(id, 0)Right(account)Right(…)

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. create("A1"). main calls create("A1"). The companion checks the id against the pattern A- followed by 1 to 6 digits.
  2. Refused. A1 has no hyphen, so create returns Left("bad account id: A1") and never calls the constructor. No Account with a bad id exists, even for a moment.
  3. create("A-1"). The next call passes A-1, which matches, so create goes on down the column.
  4. Counted and built. create adds 1 to opened and calls the private constructor with a balance of 0. Only code inside the class and its companion can make this call.
  5. Accepted. create returns Right(Account(A-1, 0 paise)). The type Either[String, Account] tells every caller that the call can fail, and with which reason.

A public constructor and a separate validate method

class Account(val id: String) with def validate(a: Account): Boolean in the companion lets every caller build an account first and forget to validate it. Make the constructor private and let the factory validate, so a bad Account cannot exist.

Returning Either is a choice for errors a caller should handle, such as a bad id typed by a user. For a mistake in the program itself, a constructor can call require(paise >= 0), which throws an IllegalArgumentException.

Scala's private, as the JVM sees it

The javap listing at the end of the accounts session shows the private constructor as public Account(java.lang.String, long), because the companion is a separate class on the JVM, and create as a public static method that forwards to the companion. The Scala compiler enforces the private constructor; Java code could still call it, so a class that Java code uses checks its rules in the constructor as well.

Checkpoint

Check: A private constructor with a validating factory

  1. Question 1 of 2 class Account private (…) has a companion whose only factory is create. What does new Account("A-2", 0) in main give?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A compile error, E173, because the private constructor can only be accessed from class Account

    With the constructor private, the factory is the only way in from Scala code, so every Account has passed its checks.

  2. Question 2 of 2 What does Account.create("A1") return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Left(bad account id: A1), with the reason

    The session printed Left(bad account id: A1). By convention Left holds the failure and Right the success.

open, final or neither decides who may extend a class

Ledger below has no modifier and Report is declared open; both are extended in another file. The session builds the pair three times: as a program, with -feature, and with -source future -feature:

extension/Ledger.scala Scala · extension/Ledger.scala
class Ledger:
  def record(entry: String): String = s"recorded $entry"

open class Report:
  def title: String = "Report"
extension/Extend.scala Scala · extension/Extend.scala
class AuditedLedger extends Ledger:
  override def record(entry: String): String = "audited, " + super.record(entry)

class MonthlyReport extends Report:
  override def title: String = "Monthly report"

@main def extend(): Unit =
  println(AuditedLedger().record("rent"))
  println(MonthlyReport().title)
Extending a plain class and an open class from another file Python · extension_session.py
from session import run

# Ledger is not open and Report is; both are extended in another file. Three builds with different options.
run("""
cd extension
scala run . --server=false
scala compile . --server=false -feature
scala compile . --server=false -source future -feature
""")

Output

$ cd extension
$ scala run . --server=false
audited, recorded rent
Monthly report
$ scala compile . --server=false -feature
$ scala compile . --server=false -source future -feature
-- Feature Warning: Extend.scala:1:28
1 |class AuditedLedger extends Ledger:
  |                            ^^^^^^
  |Unless class Ledger is declared 'open', its extension in a separate file should be enabled
  |by adding the import clause 'import scala.language.adhocExtensions'
  |or by setting the compiler option -language:adhocExtensions.
  |See the Scala docs for value scala.language.adhocExtensions for a discussion
  |why the feature should be explicitly enabled.
1 warning found

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 extension_session.py

The first two builds were silent. Only -source future, which turns on the rules planned for later versions, gave a feature warning, and only for Ledger: its extension in a separate file should be enabled with import scala.language.adhocExtensions. The Reference calls this an ad hoc extension. It is allowed, because it is useful for test fakes and quick fixes, but a class without open was not designed for it, and its author may change what a subclass depends on.

Version note

The Reference says that the warning for ad hoc extensions will be produced by default from Scala 3.4 on. On Scala 3.9.0 it still appeared only with -source future, as the session shows, so check your own builds with that option.

final closes a class completely, in its own file too:

closed/Statement.scala Scala · closed/Statement.scala
final class Statement(val lines: List[String])

class LongStatement extends Statement(Nil)
A subclass of a final class Python · closed_session.py
from session import run

# A final class cannot be extended, in this file or any other.
run("""
cd closed
!scala compile Statement.scala --server=false
""")

Output

$ cd closed
$ scala compile Statement.scala --server=false
-- [E093] Syntax Error: Statement.scala:3:6
3 |class LongStatement extends Statement(Nil)
  |      ^
  |      class LongStatement cannot extend final class Statement
  |
  | longer explanation available when compiling with `-explain`
1 error found
Compilation failed
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 closed_session.py

E093 reads class LongStatement cannot extend final class Statement. Traits and abstract classes are open by nature, so open is only written on concrete classes. Answer the questions for a class of your own:

Interactive · answer and compare Should your class be final, open or neither? Answer two questions about the class you are writing. The table marks the modifiers that fit and says why the others do not.
Who should be able to write a subclass of it?
Will tests need a fake version of it that overrides some methods?

Answer the questions above: the table then marks what fits your case and says why the rest does not.

Three ways to declare a class, against what Scala 3.9.0 did with their subclasses
Declaration Subclass in its file Subclass in another file Changing it later A fake for tests
final class Who may extend it: no one Poor refused there too: E093, cannot extend final class Poor refused with E093 Good no subclass can depend on how it works inside Poor no subclass possible; test it through its public methods
class, no modifier Who may extend it: its own file; other files ad hoc Good allowed, with no warning Mixed compiled; a feature warning only under -source future Mixed subclasses elsewhere extend it at their own risk Mixed a fake may extend it, with import scala.language.adhocExtensions
open class Who may extend it: any file, by design Good allowed, with no warning Good allowed anywhere, with no warning Poor every method a subclass may override is a promise to keep Good any test may extend it
How the answers decide
  • “Who should be able to write a subclass of it?” No one, ever: rules out class, no modifier and open class. Only final refuses every subclass. A plain class can still be extended in its own file, and in other files with a warning at most.
  • “Who should be able to write a subclass of it?” Only code in the same file: rules out final class. final refuses subclasses in its own file as well, so the variants you plan could not be written.
  • “Who should be able to write a subclass of it?” Only code in the same file: adds a caveat to open class. open invites subclasses from every file, a promise to all users that this class does not need to make.
  • “Who should be able to write a subclass of it?” Anyone: it is designed for it: rules out final class. A final class cannot be extended at all: the compiler stops every subclass with E093.
  • “Who should be able to write a subclass of it?” Anyone: it is designed for it: adds a caveat to class, no modifier. Without open, every subclass in another file is an ad hoc extension: it compiles, but -source future warns about it and asks for adhocExtensions.
  • “Will tests need a fake version of it that overrides some methods?” Yes: adds a caveat to final class. No fake can extend a final class. Put the methods tests replace behind a trait, or test the class through its public methods.

In short: final for a class nobody may change by subclassing, open for a class designed with methods to override, and no modifier while you have not decided. A sealed class, which allows subclasses only in its own file, comes with enums later in this module.

Checkpoint

Check: open, final and ad hoc extension

  1. Question 1 of 2 On Scala 3.9.0, AuditedLedger in Extend.scala extends Ledger, a class without open in Ledger.scala. When did the compiler warn?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Only with -source future (with -feature for the details)

    The Reference plans this warning for all builds, but on 3.9.0 only -source future produced it. Report, which is open, drew no warning in any build.

  2. Question 2 of 2 final class Statement and class LongStatement extends Statement(Nil) are in the same file. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: E093, class LongStatement cannot extend final class Statement

    The closed session stopped with E093. Remove final if the class is meant to be extended, or use composition.

Practise: an account that refuses bad input

The program below checks a BankAccount with a private constructor. As given, open accepts every id, and deposit and withdraw return the same account whatever the amount:

practice/BankAccount.scala Scala · practice/BankAccount.scala
// Practise: open accepts ids such as "B-42" and refuses anything else; deposit and
// withdraw return a new account, or the reason they refuse, and never change this one.
final class BankAccount private (val id: String, val balancePaise: Long):
  def deposit(paise: Long): Either[String, BankAccount] = Right(this)
  def withdraw(paise: Long): Either[String, BankAccount] = Right(this)
  override def toString = s"BankAccount($id, $balancePaise paise)"

object BankAccount:
  def open(id: String): Either[String, BankAccount] = Right(new BankAccount(id, 0))

@main def checkPractice(): Unit =
  val start = BankAccount.open("B-42") match
    case Right(account) => account
    case Left(reason) => sys.error(s"open refused B-42: $reason")
  def balance(result: Either[String, BankAccount]): Long = result match
    case Right(account) => account.balancePaise
    case Left(_) => -1
  val after = start.deposit(10_000)
  val checks = List(
    "open(B-42) starts at 0 paise" -> (start.balancePaise == 0),
    "open(42) is refused" -> BankAccount.open("42").isLeft,
    "deposit(10000) gives 10000" -> (balance(after) == 10_000),
    "deposit(-500) is refused" -> start.deposit(-500).isLeft,
    "withdraw(2500) after it gives 7500" -> (balance(after.flatMap(_.withdraw(2_500))) == 7_500),
    "withdraw(1) from 0 is refused" -> start.withdraw(1).isLeft,
    "withdraw(0) is refused" -> start.withdraw(0).isLeft,
    "start still holds 0 paise" -> (start.balancePaise == 0),
  )
  for (label, ok) <- checks do println(s"${if ok then "ok  " else "FAIL"} $label")
  println(s"${checks.count(_._2)} of ${checks.size} checks pass")
  if checks.exists(!_._2) then sys.exit(1)
The practice program as given Python · practice_session.py
from session import run

# The practice program as given: open accepts any id, and deposit and withdraw change nothing.
run("""
cd practice
!scala run BankAccount.scala --server=false
""")

Output

$ cd practice
$ scala run BankAccount.scala --server=false
ok   open(B-42) starts at 0 paise
FAIL open(42) is refused
FAIL deposit(10000) gives 10000
FAIL deposit(-500) is refused
FAIL withdraw(2500) after it gives 7500
FAIL withdraw(1) from 0 is refused
FAIL withdraw(0) is refused
ok   start still holds 0 paise
2 of 8 checks pass
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 practice_session.py

Two of the eight checks pass. Write open, deposit and withdraw in your own copy of practice/BankAccount.scala until scala run BankAccount.scala prints 8 of 8 checks pass. First hint: open answers Left for an id that is not B- followed by digits; deposit and withdraw answer Left for an amount of 0 or less, withdraw also for more than balancePaise, and otherwise return Right(new BankAccount(id, …)) with the new balance, as Account.deposit does.

Then answer the questions on classes, objects and companions, each explained as you answer it:

Practice that adapts to you

Practise this lesson

16 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 16 In class Invoice(val customer: String, val paise: Long, taxPercent: Int = 18), what is taxPercent for Invoice("Asha", 120_000)?

    Choose one answer.

    Show the answer: Question 1 of 16

    Answer: 18

    Defaults let one constructor serve several calls; the second invoice passed taxPercent = 5 by name instead.

  2. Question 2 of 16 An auxiliary constructor prints a line and then calls this(customer, 0). What does the compiler do?

    Choose one answer.

    Show the answer: Question 2 of 16

    Answer: It refuses it, because the first statement of an auxiliary constructor must call another constructor

    Every instance must pass through the primary constructor first, so an auxiliary constructor starts with this(…). A default value is usually simpler.

  3. Question 3 of 16 javap listed no field for Invoice's taxPercent, but the compiler kept Wallet's plain parameter limitPaise as a private field. Why the difference?

    Choose one answer.

    Show the answer: Question 3 of 16

    Answer: Only the body used taxPercent while building, but the method add uses limitPaise later, so its value must be kept

    A plain parameter used only in the class body is a constructor argument and nothing more; one used in a method becomes a private field, which is why w.limitPaise was refused as a private value.

  4. Question 4 of 16 Who can read a member that class Wallet declares private?

    Choose one answer.

    Show the answer: Question 4 of 16

    Answer: Wallet itself and its companion object

    private is the narrowest access in everyday code: the class and its companion. protected adds subclasses and private[pkg] a package.

  5. Question 5 of 16 billing.Ledger has a member declared private[billing]. Which code can use it?

    Choose one answer.

    Show the answer: Question 5 of 16

    Answer: Any code inside package billing and its subpackages

    private[billing] sits between private and public: visible everywhere inside billing, nowhere outside it.

  6. Question 6 of 16 Old code you are moving to Scala 3.9 marks a field private[this]. What do you write instead?

    Choose one answer.

    Show the answer: Question 6 of 16

    Answer: private, since Scala 3 deprecates private[this] and works out by itself which private members are used only through this

    Write private. Only code that reads a field through reflection may need private[C] instead, as the Reference notes.

  7. Question 7 of 16 Where does a Scala program keep a value that belongs to no single instance, such as a tax rate?

    Choose one answer.

    Show the answer: Question 7 of 16

    Answer: In an object, such as TaxRates, which has exactly one instance

    A value shared by every instance lives in an object (or as a top-level definition of the file), set up once.

  8. Question 8 of 16 A program never uses object Startup, whose body prints starting. What does the program print from it?

    Choose one answer.

    Show the answer: Question 8 of 16

    Answer: Nothing, because the body of an object runs only at its first use

    An object is set up lazily, so an object that is never used costs nothing, and its side effects never happen. Do not rely on an object's body for work that must happen at start-up.

  9. Question 9 of 16 What makes object Account the companion of class Account?

    Choose one answer.

    Show the answer: Question 9 of 16

    Answer: It has the same name and is defined in the same file

    Same name, same file. The pair then shares private members, and the object holds the class's factories.

  10. Question 10 of 16 With Account.scala as given, a program calls create("A-1"), create("B-7"), create("A-22") and create("A-3"). What does Account.openedSoFar return then?

    Type a number.

    A hint

    opened grows only when an id matches A- followed by 1 to 6 digits.

    Show the answer: Question 10 of 16

    Answer: 3 accounts

    A-1, A-22 and A-3 match the format and are counted; B-7 is refused before the counter changes. The counter is a private var of the companion, shared by the whole program.

  11. Question 11 of 16 What does this print, with Account.scala as given?

    Read the code, then choose one answer.

    Account.create("A-1") match
      case Right(a) =>
        val b = a.deposit(50_000)
        println(a)
      case Left(reason) => println(reason)
    Show the answer: Question 11 of 16

    Answer: Account(A-1, 0 paise)

    Account is immutable: every change makes a new value. Keep the result of deposit, or the deposit is lost.

  12. Question 12 of 16 A user types an account id into a form. Should the code that checks it throw with require or return an Either?

    Choose one answer.

    Show the answer: Question 12 of 16

    Answer: Return an Either, because a bad id is an expected outcome that the caller must handle, and the type says so

    Use Either for failures the program expects, such as user input, and require for broken assumptions, which should stop the program.

  13. Question 13 of 16 The javap listing showed public Account(java.lang.String, long) for the private constructor. What does that mean for Java code that uses the class?

    Choose one answer.

    Show the answer: Question 13 of 16

    Answer: Java code can call the constructor and skip create, because only the Scala compiler enforces the private

    Scala's private, between a class and its companion, is a rule of the Scala compiler. A class that Java code uses should also check its rules in the constructor.

  14. Question 14 of 16 What does the modifier open say about a class?

    Choose one answer.

    Show the answer: Question 14 of 16

    Answer: It is designed to be extended, so subclasses in other files need no feature import

    open documents an extension contract: methods meant to be overridden, and how. Without it, extending the class from another file is an ad hoc extension.

  15. Question 15 of 16 Do classes in other files need open on a trait before they can extend it?

    Choose one answer.

    Show the answer: Question 15 of 16

    Answer: No, traits and abstract classes are open by nature

    open is written only on concrete classes. Traits, the next lesson's subject, are the usual way to share behaviour.

  16. Question 16 of 16 A class computes interest with a formula that must never change, and no subclass may override it. How do you declare it?

    Choose one answer.

    Show the answer: Question 16 of 16

    Answer: final class

    final closes the class; E093 stops any subclass. Use open for classes designed to be extended, and no modifier only while you have not decided.

Interview questions

Fresher · What is a companion object in Scala, and what is it used for?

A companion object is an object with the same name as a class, defined in the same file. The class and its companion can read each other’s private members, which a separate object could not. The companion holds what belongs to the type rather than to one instance: constants, counters, helpers and, above all, factory methods, such as an apply or a create that checks its arguments and returns an Either or an Option. With the constructor made private, that factory becomes the only way for Scala code to build an instance, so no instance skips the checks. Scala 3 also puts the generated apply methods in the companion, which is why Invoice(…) works without new, unless the companion defines an apply itself. Java code reaches the companion’s methods through static forwarders on the class.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A class’s parameter list is its primary constructor: val parameters become public fields, plain ones stay private to the class, defaults replace most auxiliary constructors, and the class body runs for every instance.
  • Scala 3 generates an apply method for each constructor, so Invoice("Asha", 120_000) needs no new, unless the companion defines an apply of its own.
  • private members are visible to the class and its companion, protected ones to subclasses as well, but not to the rest of the package as in Java.
  • An object is a single instance whose body runs once, on its first use; a companion object shares private members with its class and holds its factories.
  • A private constructor and a factory that returns Either make invalid instances impossible to build.
  • Declare a class final when nobody may extend it and open when it is designed for extension; on Scala 3.9.0, extending a plain class from another file was warned about only under -source future.

Terms in this lesson

auxiliary constructor
An extra constructor written def this(…) in a class body. Its first statement must call another constructor of the class, so every instance still passes through the primary constructor.
companion object
An object with the same name as a class, defined in the same file. The two can read each other's private members, and the object holds what belongs to the type rather than to one instance, such as factory methods.
primary constructor
The constructor made of a class's parameter list and its body. Every instance is built by it: val parameters become fields, and the statements of the body run from top to bottom.
universal apply method
An apply method that Scala 3 generates for each constructor of a concrete class, so Invoice("Asha", 120_000) builds an instance without new. None is generated when the companion already has a member named apply.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.