Question
Idiomatic Logging in Kotlin: Companion Objects, Properties, and Best Practices
Question
In Java, a common logging pattern is to define a class-level static logger like this:
public class Foo {
private static final Logger LOG = LoggerFactory.getLogger(Foo.class);
}
Kotlin does not use static fields in the same way as Java. What is the idiomatic way to perform logging in Kotlin, and how can you define a logger that is reused at the class level?
Short Answer
By the end of this page, you will understand how Kotlin handles class-level logging without Java-style static fields. You will learn the most common Kotlin logging patterns, when to use a companion object, how top-level or delegated approaches can help, and what developers typically do in real Kotlin codebases.
Concept
In Java, logging is often stored in a static final field so that one logger instance is shared by the class. Kotlin does not have static members in the same form, but it provides several alternatives that serve the same purpose.
The most common Kotlin replacement is a property inside a companion object. A companion object belongs to the class rather than to individual instances, so it is the closest equivalent to a Java static member.
For example:
class Foo {
companion object {
private val log = LoggerFactory.getLogger(Foo::class.java)
}
}
This matters because logging is usually:
- shared by all instances of a class
- created once and reused
- tied to the class name for filtering and debugging
In real programs, loggers help you:
- track application flow
- record errors and warnings
- debug production issues
- monitor important events
In Kotlin, the goal is not to imitate Java syntax exactly, but to use Kotlin features to express the same idea clearly and safely.
Mental Model
Think of a logger as a notebook assigned to a class.
- If every object instance had its own notebook, you would waste space and repeat setup.
- Instead, the whole class shares one notebook.
- In Java, that shared notebook is often stored in a
staticfield. - In Kotlin, the
companion objectis the shared shelf where that notebook lives.
So a companion object is like a shared storage area for things that belong to the class itself, not to each object created from the class.
Syntax and Examples
The most common Kotlin logging syntax uses a companion object.
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class Foo {
companion object {
private val log: Logger = LoggerFactory.getLogger(Foo::class.java)
}
fun doWork() {
log.info("Work started")
}
}
Why this works
companion objectholds values shared by the classprivate valmeans the logger cannot be changed and is only visible inside the classFoo::class.javagets the JavaClassobject required by many logging libraries
Another common style
Some developers put the logger directly in the companion object without explicitly writing the type:
class Foo {
companion object {
private val log = LoggerFactory.getLogger(Foo::.java)
}
}
Step by Step Execution
Consider this example:
import org.slf4j.LoggerFactory
class PaymentService {
companion object {
private val log = LoggerFactory.getLogger(PaymentService::class.java)
}
fun process(amount: Int) {
log.info("Processing payment: {}", amount)
}
}
What happens step by step
- The
PaymentServiceclass is loaded. - Its
companion objectis created. - The
logproperty is initialized once usingLoggerFactory.getLogger(PaymentService::class.java). - When
process(100)is called, the method uses the sharedloginstance. - The logging framework records a message associated with the
PaymentServiceclass.
Important detail
Even if you create many PaymentService objects, they all use the same logger stored in the companion object.
Real World Use Cases
Logging in Kotlin is used in many real applications.
Web applications
class UserController {
companion object {
private val log = LoggerFactory.getLogger(UserController::class.java)
}
fun createUser(username: String) {
log.info("Creating user: {}", username)
}
}
Useful for tracking incoming requests and important actions.
Background jobs
class EmailJob {
companion object {
private val log = LoggerFactory.getLogger(EmailJob::class.java)
}
fun run() {
log.info("Email job started")
}
}
Useful for cron jobs, schedulers, and batch tasks.
Error reporting
class FileImporter {
companion {
log = LoggerFactory.getLogger(FileImporter::.java)
}
{
{
} (e: Exception) {
log.error(, path, e)
}
}
}
Real Codebase Usage
In real Kotlin projects, developers usually choose one of a few patterns.
1. Companion object logger
This is the most direct replacement for Java static loggers.
class OrderService {
companion object {
private val log = LoggerFactory.getLogger(OrderService::class.java)
}
}
Use this when you want a clear class-level logger with minimal magic.
2. Base helper or generic logger function
Teams often reduce repetition with a helper:
inline fun <reified T> logger() = LoggerFactory.getLogger(T::class.java)
class OrderService {
companion object {
private val log = logger<OrderService>()
}
}
This improves consistency across many classes.
3. Instance property logger
Sometimes developers use an instance-level property:
class OrderService {
log = LoggerFactory.getLogger(javaClass)
}
Common Mistakes
1. Creating a logger per instance when a class-level logger is enough
Broken style:
class Foo {
private val log = LoggerFactory.getLogger(Foo::class.java)
}
This is not always wrong, but it is usually unnecessary if all instances can share one logger.
Better:
class Foo {
companion object {
private val log = LoggerFactory.getLogger(Foo::class.java)
}
}
2. Using the wrong class reference
Broken code:
class Foo {
companion object {
private val log = LoggerFactory.getLogger(String::class.java)
}
}
This logger will be named for String, not Foo.
Better:
class {
{
log = LoggerFactory.getLogger(Foo::.java)
}
}
Comparisons
| Approach | Kotlin Example | Shared at Class Level? | Typical Use |
|---|---|---|---|
| Companion object logger | companion object { private val log = LoggerFactory.getLogger(Foo::class.java) } | Yes | Most common idiomatic class-level logging |
| Instance property logger | private val log = LoggerFactory.getLogger(javaClass) | No | Sometimes used when subclass runtime type matters |
| Top-level logger | private val log = LoggerFactory.getLogger("MyFile") | File-level | Useful for top-level functions or singleton utilities |
| Helper function | private val log = logger<Foo>() | Usually yes | Reduces repetition in larger codebases |
Companion object vs instance logger
Cheat Sheet
import org.slf4j.LoggerFactory
class Foo {
companion object {
private val log = LoggerFactory.getLogger(Foo::class.java)
}
}
Quick rules
- Use
companion objectfor a class-level logger - Use
val, notvar - Usually make the logger
private - Use
Foo::class.javato pass the class to Java logging libraries - Prefer parameterized messages:
log.info("User id: {}", id)- not
log.info("User id: " + id)
Useful helper
inline fun <reified T> logger() = LoggerFactory.getLogger(T::class.java)
Usage:
FAQ
What is the Kotlin equivalent of a static logger?
The usual equivalent is a val inside a companion object. It gives you one shared logger for the class.
Is companion object the idiomatic way to define a logger in Kotlin?
Yes, it is one of the most common and widely accepted patterns, especially when using Java logging libraries like SLF4J.
Can I create the logger as a normal property instead?
Yes, but that creates an instance property rather than a class-level property. It is usually less desirable unless you specifically need instance-based behavior.
Why do Kotlin examples use Foo::class.java?
Many logging libraries are Java libraries and expect a Java Class object. Foo::class.java converts the Kotlin class reference into the Java form.
Should the logger be private?
Usually yes. Other classes normally do not need direct access to another class's logger.
Should I name the logger log or logger?
Either is fine. log is very common because it is short and readable.
Is there a built-in logger in Kotlin?
No. Kotlin itself does not include a built-in general-purpose logging API. Projects usually use libraries such as SLF4J with Logback or Log4j.
Mini Project
Description
Create a small Kotlin service that logs different events during a user registration flow. This project demonstrates how to define a class-level logger idiomatically and use it for informational messages, warnings, and errors.
Goal
Build a Kotlin class that uses a companion object logger to record what happens during registration.
Requirements
- Create a
UserRegistrationServiceclass. - Define a class-level logger using a
companion object. - Add a
register(username: String, email: String)function. - Log an info message when registration starts.
- Log a warning if the username is blank.
- Log an error if an exception occurs.
Keep learning
Related questions
Accessing Kotlin Extension Functions from Java
Learn how Kotlin extension functions are compiled and how to call them correctly from Java with clear examples and common pitfalls.
Allow HTTP and HTTPS in Android 9 Pie with Network Security Configuration
Learn how Android 9 Pie handles cleartext HTTP traffic and how to allow HTTP and HTTPS safely using network security config.
Android AlarmManager Example: Scheduling Tasks with AlarmManager
Learn how to use Android AlarmManager to schedule tasks, set alarms, and handle broadcasts with a simple beginner example.