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" neededval c2 = new Circle(5.0) // still valid — explicit "new" always works tooval unit = Circle.unitCircleWhen 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.
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 methodcase 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 Option — Some(...) 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 rejectobject 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.goldenRatioWhere 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 Directioncase object North extends Directioncase object South extends Directioncase object East extends Directioncase 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 allMaking 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 patternclass 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.