Functional (SAM) interfaces,
explained properly.
One abstract method, one keyword, and a compiler trick that turns ceremony into a lambda. A step-by-step guide to fun interface — what it is, what the compiler does with it, and when a plain type alias is the better tool.
01The concept
A name for a very specific shape of interface.
An interface with exactly one abstract member function is called a functional interface, or a SAM interface — Single Abstract Method. It may carry any number of non-abstract members (functions with bodies), but only one method can be left abstract.
In Kotlin you opt in explicitly with the fun modifier on the interface declaration:
fun interface KRunnable { fun invoke() }
That little fun is a contract with the compiler. It says: this interface will always have exactly one abstract method, so let callers implement it with a lambda. The compiler enforces the contract — add a second abstract method and the declaration stops compiling.
02Your first one, step by step
From a plain interface to a one-liner, in five moves.
Start with an ordinary interface
Say you want a reusable "does this Int pass?" check. The classic OO answer is an interface:
interface IntPredicate { fun accept(i: Int): Boolean }
Feel the pain of implementing it
Without any help from the language, every implementation is an anonymous object — six lines to express "is it even":
// Creating an instance of a class val isEven = object : IntPredicate { override fun accept(i: Int): Boolean { return i % 2 == 0 } }
Add the fun modifier
One keyword changes what callers are allowed to do:
fun interface IntPredicate { fun accept(i: Int): Boolean }
Implement it with a lambda
Now the six lines collapse into one. The lambda's shape — takes an Int, returns a Boolean — matches the single abstract method, so the compiler wires it up for you:
// Creating an instance using a lambda val isEven = IntPredicate { it % 2 == 0 }
Use it like any other object
The result is a real IntPredicate instance — you call its method by name:
fun main() { println("Is 7 even? - ${isEven.accept(7)}") } // Output: // Is 7 even? - false
03SAM conversion, up close
What the compiler actually does when you hand it a lambda.
The one-liner in step 4 isn't magic syntax — it's a SAM conversion. When a lambda's signature matches the signature of a functional interface's single method, Kotlin converts the lambda into code that dynamically instantiates an implementation of that interface. Conceptually, you write the left tab and the compiler produces something equivalent to the right tab:
val isEven = IntPredicate { it % 2 == 0 }
val isEven = object : IntPredicate { override fun accept(i: Int): Boolean { return i % 2 == 0 } }
Two things follow from this mental model:
The lambda must match the method's signature
Parameter types and return type are checked against the single abstract method. If they don't line up, there is nothing to convert and you get a type error.
IntPredicate { ... } is a SAM constructor
Writing the interface name before the lambda is an explicit conversion. In parameter position it's often unnecessary — if a function expects an IntPredicate, you can pass a bare lambda and the conversion happens implicitly:
fun countMatching(numbers: List<Int>, predicate: IntPredicate): Int = numbers.count { predicate.accept(it) } fun main() { val nums = listOf(1, 2, 3, 4, 5, 6) // bare lambda: converted implicitly because the parameter type is known println(countMatching(nums) { it % 2 == 0 }) // 3 // works with function references too println(countMatching(nums, ::isPositive)) // 6 } fun isPositive(i: Int): Boolean = i > 0
04Rules & gotchas
What the compiler will and won't let you get away with.
Exactly one abstract method — but non-abstract members are fine
"Functional" doesn't mean "single-member." The interface can carry helper functions with bodies; only the abstract count is restricted:
fun interface Validator { fun validate(input: String): Boolean // the one abstract method fun validateOrThrow(input: String) { // has a body — fine require(validate(input)) { "Invalid: $input" } } }
Two abstract methods? Compile error.
fun interface Broken { fun first() fun second() // error: fun interface must have exactly one abstract method }
fun interface can't declare val name: String without an implementation — there'd be no way to express it in a lambda.
They're real types: extension and inheritance work
Because a functional interface creates a genuine new type, it can extend other interfaces and be the target of extension functions — capabilities you'll lean on in section 08.
05Java interop
You've been using SAM conversions all along.
SAM conversion also applies to Java interfaces with a single abstract method — no fun modifier needed, since Java interfaces can't opt in and Kotlin extends the courtesy automatically. Every time you've passed a lambda where a Java API wanted a Runnable or Comparator, this is what happened:
import java.util.concurrent.Executors fun main() { val executor = Executors.newSingleThreadExecutor() // java.lang.Runnable is a SAM interface — lambda converts implicitly executor.submit { println("running on ${Thread.currentThread().name}") } // java.util.Comparator too val byLength = Comparator<String> { a, b -> a.length - b.length } println(listOf("kotlin", "jvm", "sam").sortedWith(byLength)) executor.shutdown() }
A useful bit of history: Kotlin supported SAM conversion for Java interfaces from the very beginning, but for Kotlin's own interfaces you must opt in with fun interface. The reason is that Kotlin already has first-class function types — the language wants you to be deliberate about choosing interface semantics over a plain (Int) -> Boolean.
06Migrating legacy patterns
Retiring the "interface + factory function" idiom without breaking callers.
Before fun interface existed, a common workaround was to pair a plain interface with a same-named factory function that accepted a lambda:
The legacy shape
interface Printer { fun print() } fun Printer(block: () -> Unit): Printer = object : Printer { override fun print() = block() }
Callers could then write Printer { ... } or pass ::Printer as a reference — mimicking what SAM constructors now give you for free.
Replace both with one declaration
Since Kotlin 1.6.20, callable references to functional interface constructors are supported, making the migration source-compatible. The whole pattern collapses to:
fun interface Printer { fun print() }
Its constructor is created implicitly, so code that used the ::Printer function reference keeps compiling:
documentsStorage.addPrinter(::Printer)
Keep binary compatibility for compiled callers
Source compatibility isn't binary compatibility — already-compiled libraries still link against the old factory function. Keep it in the bytecode but hide it from new source with DeprecationLevel.HIDDEN:
@Deprecated( message = "Use the fun interface constructor instead", level = DeprecationLevel.HIDDEN ) fun Printer(block: () -> Unit): Printer = Printer(block)
HIDDEN makes the declaration invisible to newly compiled code while keeping the symbol available for binaries that already reference it.
07Functional interfaces vs. type aliases
Same shape at the call site — completely different type semantics.
The IntPredicate example can be rewritten with a type alias for a functional type, and at a glance it looks almost identical:
fun interface — new type
fun interface IntPredicate { fun accept(i: Int): Boolean } val isEven = IntPredicate { it % 2 == 0 } // invoked via the method name isEven.accept(7)
typealias — just a name
typealias IntPredicate = (i: Int) -> Boolean val isEven: IntPredicate = { it % 2 == 0 } // invoked directly isEven(7)
The difference is in what exists afterward. A type alias is just a name for an existing type — IntPredicate and (Int) -> Boolean are the same type, fully interchangeable. A functional interface creates a new nominal type, and that buys real capabilities:
| Capability | typealias | fun interface |
|---|---|---|
| Creates a new type | No — a rename only | Yes — a distinct nominal type |
| Members | One (the function shape itself) | One abstract method plus any non-abstract members |
| Can extend / implement other interfaces | No | Yes |
| Type-specific extension functions | No — they'd apply to every matching function | Yes — scoped to this interface only |
| Runtime cost | None beyond the function itself | May require conversion to the interface type |
| Call syntax | isEven(7) |
isEven.accept(7) |
The decision, in two questions
typealias to give that type a shorter, readable name. Cheapest option, zero conversion cost, maximum interchangeability.fun interface. You get a real type to hang extensions on, default helper methods, and interface inheritance — at the price of slightly more syntax and possible conversion cost.08Patterns in practice
Where fun interfaces earn their keep in real codebases.
Pattern 1 — Pluggable strategies with lambda-friendly call sites
A functional interface gives the dependency a domain name and a documented contract, while callers still get to pass a one-line lambda:
fun interface RetryPolicy { /** Returns the delay in ms before [attempt], or null to stop retrying. */ fun delayFor(attempt: Int): Long? } class WebhookDispatcher(private val retryPolicy: RetryPolicy) { // ... uses retryPolicy.delayFor(attempt) between deliveries } // Call sites read like configuration: val dispatcher = WebhookDispatcher { attempt -> if (attempt <= 5) 1000L * (1 shl attempt) else null // exponential backoff, 5 tries }
Pattern 2 — Composition through extension functions
This is the capability a type alias can't give you: extensions scoped to this interface, not to every (String) -> Boolean in the program:
fun interface Validator { fun validate(input: String): Boolean } // Extensions specific to Validator — impossible on a bare function type infix fun Validator.and(other: Validator) = Validator { validate(it) && other.validate(it) } infix fun Validator.or(other: Validator) = Validator { validate(it) || other.validate(it) } fun main() { val notBlank = Validator { it.isNotBlank() } val maxLength = Validator { it.length <= 64 } val looksLikeEmail = Validator { "@" in it } val emailField = notBlank and maxLength and looksLikeEmail println(emailField.validate("erick@example.com")) // true println(emailField.validate("")) // false }
Pattern 3 — A contract richer than a signature
When the single method carries non-trivial semantics — ordering guarantees, idempotency expectations, threading rules — the interface is the right home for that documentation. The signature (Event) -> Unit says nothing; EventHandler with a KDoc contract says everything:
fun interface EventHandler { /** * Handles a single event. * * Implementations MUST be idempotent: the same event may be * delivered more than once. Called from a single consumer * thread — no internal synchronization required. */ fun handle(event: Event) }