When designing a plugin DSL, when should you model configuration as a nested sub-extension versus flat properties on the top-level extension? What are the trade-offs?
answer
- nest cohesive concepts, flat for few/unrelated
- nesting aids IDE + reuse + @Nested fingerprint
- avoid 3+ levels deep
- one-of -> nested spec; many-named -> container
- always lazy Property fields
basics
~20 sUse nesting to group cohesive, related settings (one feature area) into a typed block for discoverability and reuse. Keep things flat when there are only a few settings or no clear grouping. Don't over-nest — deep DSLs are hard to navigate.
solid answer
~40 sNest when a set of options forms a cohesive concept (e.g. `server { }`, `compression { }`) that benefits from its own typed block: it improves IDE auto-completion, groups documentation, and lets you reuse the spec type across tasks and plugins. It also pairs naturally with `@Nested` on the consuming task so configured values fingerprint correctly. Keep flat for a handful of unrelated properties — nesting them adds ceremony with no payoff. Avoid deep nesting (three-plus levels) which hurts discoverability and makes wiring verbose. Prefer `Named` containers when users add an arbitrary number of similar items, and a single nested spec when there's exactly one of something. Always back nested values with lazy `Property` types so they wire into tasks and stay Configuration-Cache friendly regardless of nesting depth.
code
kotlin · 10 lines// Cohesive group -> nest
abstract class CompressionSpec {
@get:Input abstract val level: Property<Int>
@get:Input abstract val algorithm: Property<String>
}
abstract class MyExtension {
abstract val outputDir: DirectoryProperty // flat, unrelated single value
abstract val compression: CompressionSpec // nested cohesive concept
}
// myPlugin { outputDir.set(...); compression { level.set(9) } }go deeper
Recognize that related settings can be grouped into a block and unrelated ones kept flat.
Apply the cohesion heuristic and back nested values with lazy Property types.
Trade off nesting depth, reuse, @Nested fingerprinting, and named-container vs single-spec cardinality.
Define org-wide DSL design conventions: stable extension points, depth limits, lazy-by-default, and consistent nesting idioms across plugins.
## The design question A plugin extension is a public API. How you shape it — flat vs nested — affects readability, IDE support, reuse, incremental-build correctness, and long-term evolvability. ## When to nest - **Cohesion**: several options describe one sub-concept (TLS settings, compression settings, a remote server). A nested block names the concept and groups its options. - **Reuse**: a nested spec type can be shared between multiple tasks or even plugins, and mirrored on a task via `@Nested` so the values participate in fingerprinting. - **IDE/UX**: `myPlugin { compression { level = 9 } }` gives focused auto-completion and reads like prose. - **Evolvability**: adding fields to a nested type is a localized change; the block is a stable extension point. ## When to stay flat - Few, loosely related settings — `outputDir`, `verbose`. Wrapping them in blocks is pure ceremony. - Settings users set once and rarely together. ## When to use a Named container instead of a single nested spec If users configure an *arbitrary number* of similar items (environments, targets, publications), a `NamedDomainObjectContainer<Spec>` is the right tool — that is a different mechanism than a single nested sub-extension and is covered by its own concern. The decision: *one of something* -> nested spec; *many named of something* -> named container. ## Anti-patterns - **Over-nesting**: three or more levels deep makes scripts hard to read and wiring (`ext.a.b.c.value`) verbose and brittle. - **Eager fields under nesting**: defeats lazy wiring; always use `Property`/`ListProperty`. - **Duplicating a spec on both extension and task without @Nested**: changes won't invalidate the task. ## Worked guidance ```kotlin // Good: cohesive, one of, typed, lazy abstract class CompressionSpec { @get:Input abstract val level: Property<Int> @get:Input abstract val algorithm: Property<String> } abstract class MyExtension { abstract val compression: CompressionSpec } // myPlugin { compression { level.set(9) } } ``` ## Bottom line Nest for cohesion, reuse, and discoverability; stay flat for small/unrelated sets; reach for a named container when the cardinality is many; and keep everything lazy so depth never breaks wiring or the Configuration Cache.
- When would a NamedDomainObjectContainer be a better choice than a single nested spec?When users configure an arbitrary number of similar named items (environments, targets, publications). A single nested spec models exactly one of something; a named container models many keyed by name.
- What is the downside of deeply nested DSLs?They hurt discoverability and make value wiring verbose and brittle (`ext.a.b.c.value`). Two levels is usually the practical ceiling for readability.
saying these in an interview costs you the question
- Nesting every single property into its own block, producing a deep, ceremonial DSL.
- Using nesting but backing it with eager mutable fields, breaking lazy wiring and Configuration Cache.