Technology  /  Scala

🔴 Scala 21 guides · updated 2026

The JVM's hybrid OOP/functional language — immutability, pattern matching, and the type system that powers Spark and Akka, from first principles to production.

Companion and Singleton Objects in Scala: apply, unapply and Factory Methods

The object keyword has been mentioned in nearly every guide in this series so far — as an entry point (object Hello), and implicitly, as the thing that lets you write Point(1.0, 2.0) instead of new Point(1.0, 2.0) for a case class. This guide makes that mechanism explicit: what object actually creates, why a class and an object sharing a name get special compiler privileges, and how to write your own apply/unapply methods for classes that aren’t case classes.


object Creates a Singleton — Exactly One Instance, Ever

object Configuration {
val environment = "production"
val maxRetries = 3
def describe(): String = s"Running in $environment with $maxRetries retries"
}
println(Configuration.describe())
println(Configuration.environment)

Unlike class, which is a blueprint you instantiate with new (possibly many times), object declares both the blueprint and its single instance simultaneously — there’s no separate new Configuration() step, and there’s no way to create a second instance at all. The JVM lazily initializes the object the first time it’s referenced, then reuses that same instance for the lifetime of the program — this is precisely the Singleton design pattern, but built into the language rather than requiring the manual private-constructor-plus-static-instance boilerplate Java needs to implement the same pattern safely (especially the thread-safety concerns a hand-written Java singleton has to address explicitly).

// What this actually replaces from Java's manual singleton pattern:
// public class Configuration {
// private static final Configuration INSTANCE = new Configuration();
// private Configuration() {}
// public static Configuration getInstance() { return INSTANCE; }
// ...
// }

Scala’s object gives you this entire pattern — including guaranteed thread-safe lazy initialization — for free, with no ceremony beyond the keyword itself.


Companion Objects — A Class and Object Sharing a Name

class Circle(val radius: Double) {
def area: Double = Circle.pi * radius * radius
}
object Circle {
val pi: Double = 3.14159
def apply(radius: Double): Circle = new Circle(radius) // factory method
def unitCircle: Circle = new Circle(1.0)
}
val c1 = Circle(5.0) // calls Circle.apply(5.0) — no "new" needed
val c2 = new Circle(5.0) // still valid — explicit "new" always works too
val unit = Circle.unitCircle

When a class and an object share exactly the same name and are defined in the same source file, they become companions — and companions get a special privilege neither has with any other class/object: mutual access to each other’s private members. Circle’s instance method area can reference Circle.pi (a private-if-it-were-declared-so member of the companion object) directly, and the companion object’s methods can access the class’s private fields, with no private[this] visibility violations on either side.

mutual access to private members

(same file, same name)

apply() acts as a factory

apply() acts as a factory

class Circle(radius: Double)

(the blueprint, instantiated per call)

object Circle

(the singleton, exactly one instance)

Circle(5.0) → new instance

Circle(3.0) → new instance

This mutual-access privilege is the mechanical reason companion objects are the idiomatic place for factory methods, constants related to the class, and utility functions that conceptually belong with the class but don’t need (or shouldn’t need) a specific instance to run.


apply — Why Circle(5.0) Works Without new

object Circle {
def apply(radius: Double): Circle = new Circle(radius)
}
val c = Circle(5.0) // this is actually sugar for Circle.apply(5.0)

apply is not magic syntax — it’s an ordinary method that Scala gives special calling-convention treatment: any value followed directly by parentheses (Circle(5.0)) is interpreted as a call to that value’s apply method. This is exactly the same mechanism that makes numbers(0) work for indexing into a List (List has an apply(index: Int) method) and myFunction(x) work for calling a function value (a function literal is, under the hood, an object with an apply method) — the “everything is an object” framing from the first guide in this series comes full circle here: indexing, function calls, and factory construction are all literally the same underlying mechanism.

// This is why case classes never need "new" — the compiler auto-generates exactly this apply method
case class Point(x: Double, y: Double)
// Behind the scenes, Scala generates:
// object Point {
// def apply(x: Double, y: Double): Point = new Point(x, y)
// def unapply(p: Point): Option[(Double, Double)] = Some((p.x, p.y))
// }

This is the direct answer to “why does Point(1.0, 2.0) work without new for a case class” from the previous guide: the compiler generates a companion object with exactly this apply (and unapply, covered next) automatically, for every case class, without you writing either by hand.


unapply — The Mechanism Behind Pattern Matching

class Fraction(val numerator: Int, val denominator: Int)
object Fraction {
def apply(numerator: Int, denominator: Int): Fraction = new Fraction(numerator, denominator)
def unapply(f: Fraction): Option[(Int, Int)] = Some((f.numerator, f.denominator))
}
val half = Fraction(1, 2)
half match {
case Fraction(num, denom) => println(s"$num/$denom") // "1/2" — destructured via unapply
}

unapply runs in the opposite direction from apply: instead of taking arguments and building an instance, it takes an instance and (optionally) extracts values back out of it, returned wrapped in an OptionSome(...) if the value matches the pattern, None if it doesn’t. This is precisely the mechanism that lets case Fraction(num, denom) => destructure a Fraction inside a match — it isn’t special pattern-matching syntax specific to case classes at all; it’s calling Fraction.unapply(half) under the hood, and getting back Some((1, 2)), which the match expression then binds to num and denom.

// A custom unapply that validates, not just extracts — a "matcher" that can also reject
object Even {
def unapply(n: Int): Option[Int] = if (n % 2 == 0) Some(n) else None
}
def describe(n: Int): String = n match {
case Even(value) => s"$value is even"
case _ => s"$n is odd"
}
describe(4) // "4 is even"
describe(7) // "7 is odd"

This is genuinely one of the more elegant, under-appreciated features in Scala’s pattern-matching story: Even isn’t a data type at all — it’s a pure pattern-matching helper, an object whose entire purpose is providing an unapply that both tests a condition (returning None to signal “doesn’t match”) and extracts a value (Some(n)) when it does. This is called an extractor object, and it’s how libraries build custom, meaningful pattern-matching vocabulary beyond plain case class destructuring — regex matching (case r(group1, group2) =>) uses exactly this mechanism.


Objects as Namespaces for Utility Functions

object MathUtils {
def square(x: Double): Double = x * x
def cube(x: Double): Double = x * x * x
val goldenRatio: Double = 1.6180339887
}
MathUtils.square(4.0)
MathUtils.goldenRatio

Where a class needs no state and no instance-specific behavior at all — pure utility functions and constants — a plain (non-companion) object is the idiomatic Scala equivalent of a Java class full of static methods, without needing the static keyword at all (Scala has no static — everything that would be static in Java lives in an object instead).


Objects Extending Traits — Enum-Like Singletons

sealed trait Direction
case object North extends Direction
case object South extends Direction
case object East extends Direction
case object West extends Direction
def opposite(d: Direction): Direction = d match {
case North => South
case South => North
case East => West
case West => East
}

This is the sealed trait + case object pattern previewed in the traits and case classes guides, now shown in full — plain objects (not just case objects) can extend traits too, useful whenever you want a genuine singleton implementing a shared interface without needing the case-class-style structural equality and pattern-match destructuring case object provides (though for this exact “closed set of variants” use case, case object is almost always still the better default, precisely for that pattern-matching integration).


The “Smart Constructor” Pattern — Validating Before Construction Even Happens

class Email private (val value: String)
object Email {
def apply(value: String): Option[Email] = {
if (value.contains("@") && value.contains(".")) Some(new Email(value))
else None
}
}
Email("alice@example.com") // Some(Email@...)
Email("not-an-email") // None — invalid input never produces an Email instance at all

Making the class constructor private (via private (val value: String)) prevents anyone outside the companion object from calling new Email(...) directly — the only way to obtain an Email instance becomes the companion’s apply, which validates first and returns Option[Email] rather than throwing or silently accepting bad data. This “smart constructor” pattern is a genuinely important technique for domain modeling: it makes an invalid Email instance impossible to construct at all, rather than merely inconvenient, pushing validation to the single earliest possible point rather than scattering require checks throughout the codebase wherever an Email gets used.

// A validated, non-empty list — a common real-world use of the same pattern
class NonEmptyList[T] private (val head: T, val tail: List[T]) {
def toList: List[T] = head :: tail
}
object NonEmptyList {
def fromList[T](items: List[T]): Option[NonEmptyList[T]] = items match {
case Nil => None
case head :: tail => Some(new NonEmptyList(head, tail))
}
}

Lazy Initialization and Thread Safety

object DatabasePool {
println("Initializing connection pool...") // runs exactly once, the first time DatabasePool is referenced
val connections = (1 to 10).map(_ => new Connection())
}

An object’s initialization is guaranteed by the Scala/JVM runtime to happen exactly once, lazily (on first access), and thread-safely — even if multiple threads reference DatabasePool simultaneously for the first time, only one of them actually runs the initialization code, and the rest wait for it to complete before proceeding. This guarantee is precisely why object is the correct, idiomatic tool for shared, expensive-to-create singletons (connection pools, configuration loaded once at startup, caches) — replicating the same guarantee manually in Java requires careful use of synchronization or the “initialization-on-demand holder” idiom, both of which object provides automatically with zero extra code.

Frequently Asked Questions

Do a class and its companion object need to be in the same file? Yes — this is a hard requirement, not just convention. Mutual private-member access between a class and its companion only works if the compiler processes both declarations together, which requires them to live in the same source file.

Can an object exist without a companion class? Yes, and this is extremely common — a plain utility object (like MathUtils above) with no matching class at all is a normal, idiomatic pattern for namespacing pure functions and constants.

Is apply only useful for factory methods? No — apply is a general “make this value callable with parentheses” mechanism. Beyond object factories, it powers indexing (list(0)), function-value invocation, and any custom type where “calling it like a function” is the most natural API (a Matrix class might define apply(row, col) for element access, for instance).

Why does unapply return an Option instead of just the extracted value directly? Because pattern matching needs a way to represent “this doesn’t match” — returning None signals that cleanly, letting the match expression fall through to the next case, whereas returning a bare value would have no way to express a non-match at all.

Do I need to write apply/unapply manually often? Rarely for straightforward data classes — a case class generates both automatically. You’ll write them manually mainly for extractor objects implementing custom matching logic (like the Even example) or when building a factory method with logic beyond plain construction (validation, caching, returning a subtype based on input).


What’s Next

You now understand the mechanism behind case classes’ apply/unapply generation, singleton objects, and the extractor pattern that powers custom pattern matching. This closes out the object-oriented section of this series. Next: Immutability and Pure Functions in Scala, where we shift fully into the functional-programming half of the language — why immutability is the default, and what a genuinely “pure” function buys you.