Implement a type-safe DSL for a UI component in Kotlin

This tests your ability to design clean, hierarchical APIs using Kotlin's idiomatic features. A great answer defines model classes, then uses higher-order functions with lambdas with receiver to create a nested structure, and mentions @DslMarker for scope…
What's really being asked
This question tests your architectural thinking at a micro-level. It's not about knowing syntax, but understanding how to leverage Kotlin's unique features—specifically function types with receivers—to create an elegant, type-safe, and hierarchical API. The interviewer is checking if you can write idiomatic Kotlin that is both powerful and easy to read, moving beyond simple Java-in-Kotlin patterns. Mentioning @DslMarker demonstrates a senior-level understanding of scope control and API robustness.
The full answer
A strong answer walks through the implementation in four logical steps. First, define the data model: simple classes like Form, TextField, and Button that will hold the state. Second, create the top-level builder function, e.g., fun form(init: Form.() -> Unit): Form. Explain that this function takes a lambda with Form as its receiver, allowing the lambda block to implicitly access members of a Form instance. Third, implement the nested builders within the parent classes. For example, the Form class would have a fun textField(init: TextField.() -> Unit) method that creates a TextField, applies the configuration, and adds it to a list of children. Fourth, for a top-tier answer, introduce @DslMarker to prevent improper nesting, such as calling form methods from within a textField block, thus ensuring strict structural integrity.
The mistakes people make
A frequent mistake is implementing the pattern with regular lambdas, requiring the context to be passed explicitly as a parameter (e.g., builder.textField { field -> field.id = ... }). This works but misses the entire point of the DSL's clean, implicit context. Another red flag is correctly defining the builder functions but forgetting to actually add the created child elements to the parent's collection, resulting in a DSL that executes but builds an empty tree. Finally, candidates often struggle to explain why the DSL works, failing to articulate that a lambda with receiver (T.() -> Unit) executes its code within the scope of an instance of T, making this implicit.
What usually comes next
Expect questions that probe deeper into the implementation. 1) "How would you prevent a developer from nesting a form inside a button?" This is a direct prompt to discuss @DslMarker. 2) "How could you use an extension function here?" A good answer would be to define the top-level builder as an extension on a relevant class, like fun Activity.showForm(init: Form.() -> Unit). 3) "How would you add simple text content to a button?" This tests knowledge of operator overloading, like operator fun String.unaryPlus() to allow + "Submit" inside the button block.
A concrete example
First, define models: class Form { val children = mutableListOf<UiElement>() }, class TextField(var id: String = ""). Second, define builder functions: fun form(init: Form.() -> Unit): Form { return Form().apply(init) }, and inside Form: fun textField(init: TextField.() -> Unit) { children.add(TextField().apply(init)) }. The final usage looks clean and type-safe: val myForm = form { textField { id = "username" } }. This structure makes it impossible to call textField outside a form block.
Interview question
When designing a Kotlin UI DSL, what is the primary advantage of using a lambda with a receiver (e.g., `Form.() -> Unit`) for the configuration block?
- a.It offers superior performance by creating fewer objects than a standard lambda that takes an explicit parameter.
- b.It automatically adds the newly created UI element to its parent container, simplifying the builder function's implementation.
- c.It enables a cleaner syntax by providing an implicit `this` scope, allowing you to write `id = "name"` instead of `it.id = "name"`.Correct
- d.It enforces strict nesting rules at compile time, preventing a `form` from being defined inside a `button`.
Why? this is the answer
A lambda with a receiver (`T.() -> Unit`) executes within the scope of an instance of `T`, making its members available implicitly. This enables the clean, declarative syntax characteristic of Kotlin DSLs. Option D is a tempting distractor because preventing invalid nesting is a key feature of robust DSLs, but it is achieved using the `@DslMarker` annotation, not by the receiver lambda itself.
Just read this? Test yourself on what you have been reading.
Read the original → kotlinlang.org
You just looked this up. Could you explain it out loud?
That is the part interviews actually test. Tezvyn takes questions like this one and gives you what the interviewer is really checking, the answer that lands, and the mistake that ends the conversation, in the four minutes before your next meeting.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.
We are hiring for this. Open roles that interview on kotlin — each one lists the topics its interview covers.
See open roles