A DSL should make invalid combinations harder to express
A domain-specific language is useful when repeated configuration has a clear vocabulary. Start from the desired call site, define its invariants, and return an ordinary model that the rest of the application can inspect.
data class Plan(val title: String, val topics: List<String>)
class PlanBuilder {
var title = ""
private val topics = mutableListOf<String>()
fun topic(name: String) { require(name.isNotBlank()); topics.add(name) }
fun build(): Plan {
require(title.isNotBlank() && topics.isNotEmpty())
return Plan(title, topics.toList())
}
}
fun plan(block: PlanBuilder.() -> Unit): Plan = PlanBuilder().apply(block).build()
This separates construction from execution. Builder mutation stays local, and build checks cross-field rules. A DSL that triggers network calls during configuration becomes difficult to validate or replay.
Exercise
Create a plan with two topics and reject an empty plan. Write the same construction using a plain constructor, then decide whether the DSL actually improves the repeated use case.
Check: configuration should not silently create invalid domain objects.