RizTech Academy logo
RizTech Academy
Design Patterns, the Kotlin WayLesson 2 of 530 min

Singleton, factory and builder — mostly replaced by the language

Creational patterns are about how objects are made. The three you will meet most — Singleton, Factory, and Builder — are also the clearest demonstration of this module's theme: in Java they are real, sometimes elaborate structures; in Kotlin they mostly collapse into a keyword or a default argument. Learn the problem each solves, then see how little code Kotlin needs.

Singleton — one instance for the whole program

The problem: you want exactly one instance of something — a single configuration, a single database connection pool, a single logger.

The Java pattern is genuinely fiddly: a private constructor, a static instance, and careful double-checked locking to make it thread-safe — a dozen lines that are easy to get subtly wrong.

In Kotlin it is one keyword (the classes module covered this):

object AppConfig {
    val apiUrl = "https://api.example.com"
    var timeoutSeconds = 30
}

println(AppConfig.apiUrl)      // used directly — there is exactly one

object declares the class and its single instance, created lazily and thread-safely by the runtime. The entire Singleton pattern, correct and safe, in one word. This is the starkest example in the module: a page of careful Java becomes object.

The caveat travels with the pattern, not the language: a singleton is global state, and global mutable state (a var in an object) is hard to test and trace. Use object freely for constants and stateless services; think twice before putting mutable state in one. The best-practices instinct — prefer passing dependencies explicitly — applies here regardless of how easy object makes it.

Factory — a better way to create than a constructor

The problem: a plain constructor is limited — it cannot have a descriptive name, cannot return a cached instance or a subtype, and cannot fail cleanly with a clear message. A factory is a function whose job is to create an object, with more freedom than a constructor.

In Kotlin, a factory is just a function — usually a companion-object function or a top-level function, no "factory class" required:

sealed class Notification {
    data class Email(val to: String) : Notification()
    data class Sms(val phone: String) : Notification()

    companion object {
        // a factory: chooses the subtype and names the intent
        fun of(contact: String): Notification =
            if ("@" in contact) Email(contact) else Sms(contact)
    }
}

val a = Notification.of("kavita@example.com")   // Email
val b = Notification.of("9876500001")           // Sms

Notification.of(...) reads better than a constructor and does something a constructor cannot — choose which subtype to build from the input. Factories also shine for named construction, where the constructor's argument would be ambiguous:

class Duration private constructor(val millis: Long) {
    companion object {
        fun ofSeconds(s: Long) = Duration(s * 1000)
        fun ofMinutes(m: Long) = Duration(m * 60 * 1000)
    }
}

val d = Duration.ofMinutes(5)      // clearer than Duration(300000)

Making the constructor private and offering named factories (ofSeconds, ofMinutes) means the call site says what it means. The factory is one pattern that genuinely earns its place in Kotlin — just written as a function, not a class hierarchy.

Builder — and why Kotlin rarely needs it

The problem: constructing an object with many optional parameters. In Java, a constructor with ten parameters is unusable (you cannot tell the arguments apart, and you cannot skip the middle ones), so the Builder pattern was invented: a helper object with a method per field and a final build() call.

// the Java-style Builder — a lot of machinery
val pizza = Pizza.Builder()
    .size("large")
    .cheese(true)
    .toppings(listOf("mushroom"))
    .build()

Kotlin makes this almost entirely unnecessary, because default and named arguments do exactly what the Builder was invented to provide:

data class Pizza(
    val size: String,
    val cheese: Boolean = true,
    val toppings: List<String> = emptyList()
)

val pizza = Pizza(size = "large", toppings = listOf("mushroom"))   // named args, defaults for the rest

One data class, and construction reads like the Builder did — each field named, the unwanted ones defaulted — with none of the builder machinery. When you have default and named arguments, you almost never need a Builder. This is the pattern Kotlin dissolves most completely.

There is one honest exception: a Builder can still be worth it for a very complex object built up across several steps or conditionally (add a topping only if in stock, in a loop). Even then, Kotlin often reaches for a small DSL (a pizza { ... } block using a builder lambda) rather than the classic pattern — a technique the frameworks you will use (like Jetpack Compose) lean on heavily. But for ordinary construction, named arguments win.

The scoreboard

Pattern Java Kotlin
Singleton Private constructor + static + locking object (one keyword)
Factory Often a factory class A function (companion or top-level) — still useful
Builder A builder class with a method per field Default + named arguments (rarely a Builder)

The lesson to carry: two of these three all but vanish, and the one that survives (Factory) does so as a plain function. When you catch yourself about to write a builder class or a thread-safe singleton idiom in Kotlin, stop — the language almost certainly has a one-line answer.

Check your work

What Singleton solves, and Kotlin's answer. Exactly one instance; object — one keyword, lazy and thread-safe.

The caveat that travels with Singleton. It is global state; avoid mutable state in it and prefer passing dependencies explicitly.

What a Factory gives you over a constructor. A descriptive name, the ability to choose a subtype or return a cached instance, and cleaner failure.

How a Factory is written in Kotlin. As a function — a companion-object or top-level function, not a factory class.

The private constructor + named factories pattern. Hide the constructor, offer ofSeconds/ ofMinutes so call sites read clearly.

What Builder solves, and why Kotlin rarely needs it. Constructing objects with many optional parameters; default and named arguments do the same directly.

The one time a Builder (or DSL) still helps. A very complex object built up conditionally across steps.

The overall scoreboard. Singleton → object; Factory → a function (still useful); Builder → default/named arguments.

Practice

  1. Write a Singleton as an object and confirm you cannot create a second instance.
  2. Write a Notification.of(contact) factory that returns an Email or Sms subtype based on the input. Test both branches.
  3. Write a Duration class with a private constructor and ofSeconds/ofMinutes factories. Confirm the constructor cannot be called directly.
  4. Write a Pizza data class with defaults, and construct one with named arguments, skipping the defaulted fields. Compare with an imagined Builder.
  5. Take a Java-style Builder you can find online and rewrite it as a Kotlin data class with default arguments.
  6. For a config object with eight optional fields, decide whether you need any pattern at all, and justify your answer.

Official documentation

Next: structural patterns — Adapter, Decorator, and delegation with extensions and by.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship