Question

Kotlin Companion Object Explained: Static Replacement, Meaning, and Best Practices

kotlinkotlin-companion

Question

In Kotlin, I have mostly used a companion object as a replacement for Java static members whenever I need class-level data or behavior.

I want to understand the intended meaning and proper use of companion object more clearly:

  • Why is it called a companion object?
  • If I need multiple static-like properties or functions, do they all need to be grouped inside a single companion object block?
  • If I want a singleton instance associated with a class, I sometimes write code like this:
class MyService {
    companion object {
        val singleton by lazy { MyService() }
    }
}

This works, but it feels a little unidiomatic. What is the better Kotlin way to handle this?

Short Answer

By the end of this page, you will understand what a companion object is in Kotlin, why it exists, how it differs from Java static, and when it is the right tool. You will also learn idiomatic ways to define constants, factory methods, and singleton-style objects in Kotlin.

Concept

Kotlin does not have static members in the same way Java does. Instead, Kotlin uses objects for shared, single-instance behavior.

A companion object is a special object declared inside a class that is tied to that class definition. It acts like the class's built-in companion: a singleton object that lives alongside the class rather than inside each instance.

Why Kotlin uses this design

In Java, static means “belongs to the class, not to any object instance.” Kotlin takes a more object-oriented approach:

  • shared behavior is still represented by an object
  • that object can have properties, functions, interfaces, and inheritance
  • the object can be named or left as the default Companion

This gives Kotlin more flexibility than a plain static keyword.

Why it is called "companion"

It is called a companion object because it is an object that accompanies a class. It is closely associated with the class and can access that class's private members.

Example:

class User private constructor(val name: String) {
    companion object {
        fun create(name: String): User {
            return User(name)
        }
    }
}

Here, the companion object is not an instance of User. It is a separate singleton object attached to User.

One companion object per class

A class can have only one companion object.

So if you want multiple static-like properties and functions, yes, they are grouped into that one companion object:

class Config {
    companion object {
        const val DEFAULT_PORT = 8080
        fun parse(value: String): Int = value.toInt()
        val supported = listOf("dev", "prod")
    }
}

That does not mean Kotlin forces everything into one giant block globally. It just means each class gets at most one built-in companion.

Companion object vs singleton object

A common beginner confusion is this:

  • Use a companion object when the value or function logically belongs to a class.
  • Use a top-level object declaration when you want a standalone singleton.

If your goal is simply “I need one global instance,” a plain object is often better:

object MyService {
    fun doWork() {
        println("Working")
    }
}

This is more idiomatic than creating a class plus a companion-held singleton when you never need multiple instances.

When companion object is a good fit

A companion object is commonly used for:

  • factory methods
  • constants related to the class
  • parsing helpers
  • values that conceptually belong to the class itself
  • interoperability with Java static members using @JvmStatic or @JvmField when needed

When your by lazy example is not ideal

This pattern:

class MyService {
    companion object {
        val singleton by lazy { MyService() }
    }
}

is valid Kotlin, but it suggests a design question:

  • Do you really need a class with many instances possible, plus one special shared instance?
  • Or do you actually want exactly one instance in the whole program?

If exactly one instance is needed, prefer:

object MyService

If you need normal instances and also want a default shared instance, then a companion property can be reasonable:

class MyService {
    companion object {
        val defaultInstance by lazy { MyService() }
    }
}

That is not wrong. It is useful when the class supports both custom instances and a commonly shared one.

Mental Model

Think of a class as a blueprint for objects.

Each time you create an instance, you build one house from the blueprint.

A companion object is like a small office attached to the blueprint itself:

  • it is not one of the houses
  • it exists only once
  • it stores information and tools related to the blueprint

So:

  • instance properties belong to each house
  • companion object members belong to the office attached to the blueprint

A plain object declaration is different: that is just one building that already exists, not a blueprint plus an attached office.

Take Quiz

Syntax and Examples

Basic syntax

class Example {
    companion object {
        const val VERSION = "1.0"

        fun greet() {
            println("Hello from companion object")
        }
    }
}

Use it like this:

fun main() {
    println(Example.VERSION)
    Example.greet()
}

Named companion object

A companion object can have a name:

class MathHelper {
    companion object Factory {
        fun double(x: Int) = x * 2
    }
}

You can call it in either of these ways:

println(MathHelper.double(5))
println(MathHelper.Factory.double(5))

Companion object for factory methods

class User private constructor(val name: String) {
    companion object {
        fun fromEmail(email: String): User {
            val name = email.substringBefore("@")
            return User(name)
        }
    }
}

fun main() {
    val user = User.fromEmail("alex@example.com")
    println(user.name)
}

This is a common pattern when object creation needs logic.

When to use object instead

object Logger {
    fun log(message: String) {
        println("LOG: $message")
    }
}

Use this when you need one singleton object, not a class plus class-level members.

Shared instance attached to a class

class DatabaseConnection(val url: String) {
    companion object {
        val shared by lazy { DatabaseConnection("localhost") }
    }
}

This is useful when both of these are valid:

val a = DatabaseConnection.shared
val b = DatabaseConnection("remote-server")

Step by Step Execution

Consider this example:

class Counter {
    companion object {
        var created = 0
    }

    init {
        created++
    }
}

fun main() {
    println(Counter.created)
    val a = Counter()
    val b = Counter()
    println(Counter.created)
}

Step-by-step

1. The class is defined

Counter has:

  • a companion object with a property created
  • an init block that runs every time a Counter instance is created

2. println(Counter.created) runs

At this point, no Counter instances exist yet.

Output:

0

3. val a = Counter() runs

A new Counter instance is created.

The init block runs and does:

created++

Now created becomes 1.

4. val b = Counter() runs

Another Counter instance is created.

The init block runs again.

Now created becomes 2.

5. println(Counter.created) runs again

Output:

2

What this shows

  • created is shared across all instances because it lives in the companion object
  • a and b are separate instances
  • the companion object itself exists once per class, not once per instance

Real World Use Cases

Common practical uses

Constants related to a class

class HttpClient {
    companion object {
        const val DEFAULT_TIMEOUT_MS = 5000
    }
}

Useful when a constant is strongly tied to the meaning of a class.

Factory methods

class Order private constructor(val id: String) {
    companion object {
        fun create(id: String): Order = Order(id.trim())
    }
}

Useful when creating an object requires validation or formatting.

Parsing and conversion helpers

class Color(val red: Int, val green: Int, val blue: Int) {
    companion object {
        fun fromHex(hex: String): Color {
            val clean = hex.removePrefix("#")
            return Color(
                clean.substring(0, 2).toInt(16),
                clean.substring(2, 4).toInt(16),
                clean.substring(4, 6).toInt(16)
            )
        }
    }
}

Shared default instance

class ApiClient(val baseUrl: String) {
    companion object {
        val default by lazy { ApiClient("https://api.example.com") }
    }
}

Useful when the class may have custom instances, but one common shared instance is convenient.

Java interoperability

In mixed Kotlin/Java projects, companion objects can expose class-level APIs in a Java-friendly way using annotations such as @JvmStatic.

Take Quiz

Real Codebase Usage

In real projects, developers usually use companion objects in a few predictable ways.

1. Named constructors and factory methods

Instead of exposing many constructors, a class may provide clear creation methods:

class Token private constructor(val value: String) {
    companion object {
        fun fromHeader(header: String): Token {
            return Token(header.removePrefix("Bearer "))
        }
    }
}

This improves readability and validation.

2. Constants and configuration defaults

class CacheManager {
    companion object {
        const val MAX_ITEMS = 100
    }
}

This keeps class-specific constants close to the class.

3. Validation before object creation

class Email private constructor(val value: String) {
    companion object {
        fun create(value: String): Email {
            require("@" in value) { "Invalid email" }
            return Email(value)
        }
    }
}

This is a common guard-clause pattern.

4. Shared default instances

class JsonParser(val strict: Boolean) {
    companion object {
        val default = JsonParser(strict = true)
    }
}

This is acceptable when both shared and custom instances are useful.

5. Interface implementation in a companion object

A companion object can implement interfaces, which static in Java cannot do directly:

interface Factory<T> {
    fun create(): T
}

class Session {
    companion object : Factory<Session> {
        override fun create(): Session = Session()
    }
}

This is one of the reasons Kotlin models class-level behavior as objects.

What developers avoid

Developers usually avoid putting unrelated utility functions into a companion object just because they feel static-like. If code is not conceptually tied to the class, a top-level function or separate object is often cleaner.

Common Mistakes

1. Using a companion object when a plain object is better

Less ideal

class AppLogger {
    companion object {
        fun log(message: String) {
            println(message)
        }
    }
}

If AppLogger will never have normal instances, this is simpler:

object AppLogger {
    fun log(message: String) {
        println(message)
    }
}

2. Treating companion objects as exactly the same as Java static

They are similar in usage, but not identical in meaning.

A companion object is an actual object, so it can:

  • have its own type
  • implement interfaces
  • be passed around as a value

3. Forcing unrelated values into a class companion

Broken design example:

class User {
    companion object {
        fun parseDate(text: String) = text
        fun connectToDatabase() = Unit
        fun calculateTax(amount: Double) = amount * 0.2
    }
}

These do not all clearly belong to User. Prefer placing code where it logically fits.

4. Assuming you can declare multiple companion objects

This is invalid:

class Example {
    companion object {
        val a = 1
    }

    companion object {
        val b = 2
    }
}

A class can only have one companion object.

5. Using a companion singleton when a top-level value is enough

Sometimes a top-level declaration is clearer:

val defaultTimeout = 5000

If the value is not strongly tied to one class, do not force it into a companion object.

6. Forgetting intent when creating shared instances

If you write:

class Service {
    companion object {
        val singleton by lazy { Service() }
    }
}

ask yourself:

  • Should there be only one Service? Use object Service.
  • Should there be many services plus one default? Then this pattern is fine, but name it clearly, such as default or shared.

Comparisons

ConceptWhat it representsBest use caseExample
companion objectOne singleton object attached to a classClass-level functions, constants, factoriesUser.create()
object declarationOne standalone singletonGlobal service or manager with exactly one instanceobject Logger
instance membersData and behavior per objectState unique to each created objectuser.name
top-level function/propertyFile-level declarationUtilities or values not tied to one classfun parsePort()

Companion object vs Java static

FeatureJava staticKotlin companion object
Belongs to classYesActs similarly
Is a real objectNoYes
Can implement interfacesNoYes
Can hold properties/functionsYesYes
Access syntaxClassName.memberClassName.member

Companion object vs plain object

Featurecompanion objectobject
Attached to a classYesNo
One per classYesNot applicable
Good for factory methodsYesSometimes
Good for true singleton serviceSometimesYes

Quick rule of thumb

  • Use companion object for things that belong to a class.
  • Use object for something that itself is the one shared instance.
  • Use top-level declarations for helpers not tied to a specific class.
Take Quiz

Cheat Sheet

Core idea

A companion object is a singleton object attached to a class.

Syntax

class Example {
    companion object {
        const val MAX = 10
        fun create(): Example = Example()
    }
}

Access

Example.MAX
Example.create()

Named companion

class Example {
    companion object Factory
}

Rules

  • A class can have only one companion object.
  • A companion object is a real object.
  • It can contain properties, functions, and interface implementations.
  • It is commonly used for constants, factories, and class-level helpers.

Use object instead when

  • you need exactly one instance in the whole program
  • you do not need separate class instances
object ConfigManager

Use a companion shared instance when

  • the class still supports normal instances
  • but one default/shared instance is useful
class Client {
    companion object {
        val shared by lazy { Client() }
    }
}

Common naming

  • Companion (default)
  • Factory
  • default
  • shared

Edge case

If the code must look like Java static from Java callers, you may need:

@JvmStatic
fun create(): Example = Example()

FAQ

Why does Kotlin use companion objects instead of static?

Kotlin models class-level behavior as objects. This is more flexible because the companion object is a real object that can hold state, implement interfaces, and be passed around.

Why is it called a companion object?

Because it is an object that stays alongside a class and is closely associated with it.

Can a Kotlin class have more than one companion object?

No. A class can have only one companion object.

Should I use a companion object for singletons?

Only if the singleton is specifically a shared instance related to a class that can also have normal instances. If you need exactly one instance overall, prefer an object declaration.

Is companion object the same as Java static?

Not exactly. It often fills the same role, but it is implemented as a real singleton object rather than a language-level static member.

Can companion objects access private constructors?

Yes. This is why they are often used for factory methods.

When should I use top-level functions instead of a companion object?

Use top-level declarations when the function or property is not strongly tied to a specific class.

Is val singleton by lazy { ... } inside a companion object wrong?

No. It is valid and sometimes useful. But if the whole type should only ever have one instance, object is usually simpler and more idiomatic.

Take Quiz

Mini Project

Description

Build a small Kotlin class that represents an application configuration. The class should support normal instances, but also provide class-level helpers using a companion object. This demonstrates when a companion object is useful for constants, factory methods, and a shared default instance.

Goal

Create a class with instance data plus a companion object that exposes a constant, a factory method, and a shared default configuration.

Requirements

  • Create a AppConfig class with at least two instance properties.
  • Add a companion object with one constant.
  • Add a factory method that builds an AppConfig from an environment name such as dev or prod.
  • Add a shared default instance in the companion object.
  • Show usage of both custom instances and the shared instance.
Take Quiz

Keep learning

Related questions

View more