Use Jilt staged builders for complex test configurations
Context and Problem Statement
For tests around linked files we frequently need BibTestConfiguration instances with multiple required and optional parameters:
@Builder(style = BuilderStyle.STAGED_PRESERVING_ORDER)
BibTestConfiguration(
String bibDir,
@Opt String librarySpecificFileDir,
@Opt String userSpecificFileDir,
String pdfFileDir,
FileTestConfiguration.TestFileLinkMode fileLinkMode,
Path tempDir
) throws IOException {
}
Usage in tests currently follows a staged builder style:
BibTestConfigurationBuilder
.bibTestConfiguration()
.bibDir("source-dir")
.pdfFileDir("source-dir")
.fileLinkMode(TestFileLinkMode.RELATIVE_TO_BIB");
// later in the code
BibTestConfiguration sourceBibTestConfiguration =
sourceBibTestConfigurationBuilder.tempDir(tempDir).build();
We want a clear, type-safe, and readable way to construct such configurations. Especially important is the ordering of the “setters” to increase code readability.
Decision Drivers
- Make test setup code more readable and explicit.
- Enforce required vs. optional parameters at compile time.
- Keep the order of builder calls aligned with the parameter declaration order.
- Avoid handwritten builder boilerplate and its maintenance.
- Provide a reusable template for future configuration/value classes.
Considered Options
- Jilt staged builder (
@Builder(style = BuilderStyle.STAGED_PRESERVING_ORDER)). - Plain constructors (direct
new BibTestConfiguration(…)), setters, andinitialize()method. - Handwritten classic builder.
- Lombok’s
@Builder.
Decision Outcome
Chosen option: “Jilt staged builder (@Builder(style = BuilderStyle.STAGED_PRESERVING_ORDER))”, because it gives us:
- type-safe staged construction,
- stable call order matching parameter order,
- clear distinction between required and optional parameters,
- no handwritten builder code.
Consequences
- Good, because test code is more readable – parameter names are visible at the call site instead of a long constructor argument list.
- Good, because required parameters are enforced by the compiler; tests cannot accidentally omit them.
- Good, because builder invocation order matches declaration order, which makes it easier to compare code and constructor signature.
- Good, because no runtime dependency is introduced; Jilt is an annotation processor only.
- Bad, because another annotation processor is in the toolchain, which slightly increases build complexity and can interact with other processors.
- Bad, because developers must learn the Jilt conventions, especially staged builders and generated builder class naming.
Confirmation
BibTestConfigurationBuilderand similar builders are generated successfully during the build.- Tests compile only if all required fields are set via the staged builder.
Pros and Cons of the Options
Jilt staged builder (@Builder(style = BuilderStyle.STAGED_PRESERVING_ORDER))
Homepage: https://github.com/skinny85/jilt
- Good, because it generates staged builders with clear required/optional semantics.
- Good, because
BuilderStyle.STAGED_PRESERVING_ORDERkeeps builder call order aligned with parameter order. - Good, because no runtime dependency; Jilt is compile-time only.
- Good, because it is compatible with new Java releases.
- Good, because it integrates with existing Java code without changing runtime behavior.
- Bad, because adding an annotation processor requires IDE/build configuration and developer knowledge.
- Bad, because navigating to generated code can be less convenient, depending on IDE support.
Plain constructors (direct new BibTestConfiguration(…)), setters, and initialize() method
- Good, because useful for very small classes.
- Good, because no extra dependency.
- Good, because IDEs understand plain constructors well.
- Bad, because more complicated calling code - and unusual calling code.
- Bad, because call sites with many parameters are hard to read and fragile.
- Bad, because no compile-time enforcement of “required vs. optional” beyond primitive defaults and
null. - Bad, because changing constructor parameter order can silently break call sites.
Handwritten classic builder
- Good, because familiar builder pattern; no extra tools.
- Good, because call sites are readable and explicit.
- Bad, because builder code is boilerplate and must be maintained manually.
- Bad, because easy to introduce inconsistencies between builder and target class.
- Bad, because type-safety of required/optional parameters might be weaker than staged builders.
Lombok’s @Builder
- Good, because concise annotation, common in many Java projects.
- Good, because builder usage is clear at call sites.
- Bad, because Lombok adds its own ecosystem and tooling constraints.
- Bad, because Lombok is not always compatible with the most recent Java versions.
- Bad, because staged/type-safe builders for required/optional distinction are less explicit than with Jilt.
- Bad, because we avoid additional Lombok usage where possible.