mylist.first();
/* there isn’t first() method
in mylist collection*/
Ivan Ponomarev / NDC Copenhagen 2025
![]() | Ivan Ponomarev
|
Strengths and weaknesses of Kotlin DSL compared to other options for creating DSLs
Several examples of tasks solved using Kotlin DSL
Some advanced specifics of designing Kotlin DSL
Basics of creating Kotlin DSL (but there will be references to relevant sources)
This talk is AI free!
![]() | Martin Fowler, Rebecca Parsons ![]() |
![]() |
|
![]() |
|
![]() |
|
![]() |
|
* Does not mean that you are safe, e.g. google for "Billion_laughs_attack".
Kotlin DSLs belong to the class of internal DSLs based on statically typed programming languages
This has both advantages and disadvantages
The choice is yours!
![]() | Dmitry Zhemerov, Svetlana Isakova |
Anton Arhipov
Kotlin DSL in under an hour
https://www.youtube.com/watch?v=0DJqr4FZ6f0
Tool | DSL syntax | General syntax |
Extension functions |
|
|
Infix functions |
|
|
Operators overloading |
|
|
Type aliases |
| Creating empty inheritors classes and other duct tapes |
Tool | DSL syntax | General syntax |
get/set methods convention |
|
|
Destructuring declaration |
|
|
Lambda out of parentheses |
|
|
Lambda with receiver (the main tool) |
| N/A |
Context control |
| N/A |
Our domain:
Conditions
Transformations
Rules
If a set of conditions is met, the necessary transformation is triggered
fun main() {
if (conditionOneMet() && conditionTwoMet()) {
runTransformationA()
} else if ((ConditionThreeMet() || ConditionFourMet()) && conditionOneMet()) {
runTransformationB()
} else if (conditionTwoMet()) {
runTransformationC()
}
}
Step-by-step debugging!
Testing with an eye on coverage (although substituting conditionXmet()
and runTransformationX()
with mocks may be challenging)
The code grows and quickly becomes difficult to read/maintain
private fun rules(): List<Rule> = listOf(
Rule(ConditionII, TransformationC),
Rule(Not(ConditionIV), TransformationB),
Rule(And(ConditionI, ConditionII), TransformationA),
Rule(Or(And(ConditionIII, ConditionIV), ConditionI), TransformationB)
)
fun main() {
rules()
.firstOrNull { it.condition.met() }
?.transformation?.run()
}
This can be written in Java, but it turned out more compact in Kotlin due to the absence of new
.
The visual connection between rules and transformations is now obvious
Step-by-step debugging has turned into a nightmare
val rules: List<Rule> =
// @formatter:off
rules {
ConditionI and ConditionIV invokes TransformationA
ConditionII invokes TransformationC
not(ConditionIV) invokes TransformationB
(ConditionI and not(ConditionIII)) invokes TransformationA
(ConditionIII
and ConditionIV
or ConditionI) invokes TransformationB
}
// @formatter:on
The method parameter rules
is a lambda with a receiver
and
, or
, not
, invokes
are infix extension functions
fun main() {
rules.firstOrNull { it.condition.met() }?.transformation?.run()
}
Advantage: all the code is a one-liner
Disadvantage: step-by-step debugging is impossible (but this can be compensated, see below)
for (rule in rules) {
rule.visit(::visitor)
}
class Rule(
val condition: Condition,
val transformation: Transformation) : Element {
override fun visit(visitor: (Element) -> Unit) {
condition.visit(visitor)
transformation.visit(visitor)
visitor.invoke(this)
}
}
"Almost free" representation of DSL in JSON/YAML/XML formats using tools like FasterXML Jackson.
Ideal for building WebUI with forms for displaying/editing settings (projectional editors)
- condition:
And:
a:
ConditionI: {}
b:
ConditionIV: {}
transformation:
TransformationA: {}
Code that generates 2N combinations of sets of fulfilled conditions,
where N is the number of subclasses of BasicCondition
private val conditions = BasicCondition::class.sealedSubclasses
fun outcomes(): Sequence<Set<ConditionClass>> = sequence {
for (i in 0L until (1L shl conditions.size)) { // this is where 2^N emerges
val activeConditions = mutableSetOf<ConditionClass>()
for (j in 0 until conditions.size) {
if ((i and (1L shl j)) != 0L) {
activeConditions.add(conditions[j])
}
}
yield(activeConditions)
}
}
"There are no unreachable rules"
"For a fixed condition, every rule of a certain class is achievable"
… etc. — it all depends on your task
We test the model itself, defined in the DSL, not the result of its interpretation!
Execution of rules
Documentation
Visualization
Validation
Serialization ("free" JSON/YAML based DSL version for our Kotlin DSL)
// Extension point in the builder
fun customCondition(lambda: () -> Boolean): Condition =
Condition(lambda)
// Using the extension point
customCondition { Random.nextDouble() < .88} invokes TransformationC
// Some other DSL
customBusinessRule { checkSmthProgramatically() }
A powerful approach for modeling business rules (e.g. state transitions), especially when a DSL can capture only part of the logic.
Depending on context, this can be a good practice — or a pitfall.
Combined with Kotlin Scripting, it unlocks scripting capabilities for your applications.
In Groovy we can omit parentheses when calling a method!
Groovy Gradle DSL:
implementation 'com.acme:example:1.0'
Kotlin Gradle DSL:
implementation ("com.acme:example:1.0")
this
val ndcCopenhagen = conference {
//Cannot be called without parentheses (unlike Groovy)
session("Edge-native applications - what happened to cloud-native?") deliveredBy {
speaker("Mikkel Mørk Hegnhøj")
speaker("Thorsten Hans")
} inRoom ("Room 3")
session("One Source to Rule Them All: Kotlin DSLs") deliveredBy {
speaker("Ivan Ponomarev")
//May be called without (..) -- infix fun
} inRoom "Room 3"
}
this
: Is it cleaner or is it less intutitive?
val ndcCopenhagen = conference {
+ "Edge-native applications - what happened to cloud-native?" deliveredBy {
+ "Mikkel Mørk Hegnhøj"
+ "Thorsten Hans"
} inRoom "Room 3"
+ "One Source to Rule Them All: Kotlin DSLs" deliveredBy {
+ "Ivan Ponomarev"
} inRoom "Room 3"
}
For the DSL we wouldn’t want this, but a lambda with a receiver does not prohibit it:
conference { //this: ConferenceBuilder
session ("Session 1") deliveredBy {
//this: ConferenceBuilder.SpeakersBuilder,
//but methods from ConferenceBuilder are also available
session (...) // ???!!!
}
}
@DslMarker
annotation class ConferenceDsl
@ConferenceDsl
class ConferenceBuilder { ... }
@ConferenceDsl
class SpeakersBuilder { ... }
Extended IDE support is promised (though not documented), so it makes sense to annotate DSL builders with @DslMarker in any case.
val ndcCopenhagen = conference {
val ip by speaker("Ivan Ponomarev", "Synthesized")
val mj by speaker("Mark Jervelund", "Microsoft")
session("One Source to Rule Them All: Kotlin DSLs") deliveredBy {
+ip
} inRoom "Room 6"
session("Part 1/2: Introduction to capture the flag (CTF)") deliveredBy {
+mj
} inRoom "Room 4"
session("Part 2/2: Introduction to capture the flag (CTF)") deliveredBy {
+mj
} inRoom "Room 4"
}
val ip = …
looks like imperative code, but there’s nothing to be done about it.
Groovy offers much more possibilities here.
//ConferenceBuilder
val speakers = mutableMapOf<String, Speaker>()
//The function returns a delegate for a read-only property
fun speaker(name: String, company: String): ReadOnlyProperty<Nothing?, Speaker> =
ReadOnlyProperty { _, property ->
//property.name contains the variable's name
speakers.computeIfAbsent(property.name) { Speaker(it, name, company) }
}
//The language syntax itself will ensure the uniqueness of identifiers
val mmh by speaker("Mikkel Mørk Hegnhøj", "Fermyon")
val th by speaker("Thorsten Hans", "Fermyon")
session("Edge-native applications - what happened to cloud-native?") deliveredBy {
+mmh
+th
} inRoom "Room 3"
Classic pattern
fun verb(action: EntityBuilder.()->Unit) {
val builder = EntityBuilder()
builder.action()
...
}
allows verb { …action… }
but not verb.property = value
object conference {
//The default method with a lambda parameter
operator fun invoke(action: ConferenceBuilder.() -> Unit): List<Talk> {
val builder = ConferenceBuilder()
builder.action()
return builder.talks;
}
//additional properties
var name: String = ""
}
https://ajalt.github.io/clikt/ (DSL for command line parameters parsing)
Gradle Kotlin DSL
Ktor Framework: https://ktor.io/
Koog AI https://docs.koog.ai/
DSL combined with design patterns is a powerful tool for solving multiple tasks.
Creating DSLs in Kotlin is not scary. You can improve parts of your existing internal APIs today making them "DSL-like".
Internal Kotlin DSLs are not the only way to implement DSLs, but definitely not the worst one in many scenarious.
Code and slides are available on GitHub https://github.com/inponomarev/dsl-talk | @inponomarev |