RecordBuilder, an annotation processor that gives Java records a builder and withers
Record builder generator for Java records
At a glance
- What is it?
- RecordBuilder is a Java annotation processor that generates a companion builder class, wither methods and even whole records from an interface template. It fills a gap in Java 16 records and it does so at compile time, with no runtime dependency.
- Who is it for?
- RecordBuilder suits Java teams working with records on Java 17 and above who want builder and wither ergonomics without hand-writing boilerplate per type, and it does not suit projects that have decided to avoid annotation processing or that need behaviour the generated class cannot express.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 99 days ago.
- What is it written in?
- Mainly Java, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 6, 2026, and from our analysis. They are not legal advice.
Star history
- Randgalt/record-builder935 ★ · +2
| Repository | 2026-10-21 | 2026-11-06 |
|---|---|---|
| Randgalt/record-builder | 933 | 935 |
2 Stars in the last 16 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The gap in Java records that this processor fills
Java 16 introduced records, and the project states the problem plainly: this version of records is fantastic, but it is missing some features normally found in data classes, specifically a builder and withers. RecordBuilder is the annotation processor that fills that gap.
It generates three things. A companion builder class for Java records, an interface that adds with copy methods, and an annotation that generates a Java record from an interface template. The third one is the least discussed and the most interesting, because it inverts the usual direction of code generation: instead of a record producing a builder, an interface produces the record.
The processor is published to Maven Central as io.soabase.record-builder:record-builder. The build matrix is visible in the badges at the top of the file, with separate Maven builds for Java 17, Java 21 and Java 24, so the generated code is compiled and tested against three JDK lines rather than one. The licence field on the repository says Apache-2.0, which is the permissive choice for a library that sits in other people's build pipelines.
What the generated builder class actually contains
The annotation goes on a record and nothing else is needed:
@RecordBuilder
public record NameAndAge(String name, int age){}What you get is a full builder class, and the generated code is worth reading because it defines the contract you will be coding against. There is a static factory builder() that returns a builder with all fields set to default Java values, and an overload builder(NameAndAge from) that returns a builder pre-filled from an existing record instance. That second overload is the copy constructor pattern in builder form, and it is the call to reach for when you want to change one field of an existing value.
There is also a static constructor named after the record, so you can call NameAndAge("hey", 42) after a static import instead of writing new. A static stream(NameAndAge record) returns the record components as map entries keyed by component name. Each component gets a setter that returns the builder for chaining and a getter with the component name, so name(String) sets and name() reads. build() returns a new record instance from the current field values, and the class also gets toString, hashCode and equals implementations.
That last detail has consequences. The builder is a mutable object with value equality of its own, which is convenient for tests that compare two configured builders and slightly awkward when you wanted a simple mutable holder.
Withers through the generated With interface
The wither support is separate from the builder and opt-in per record. A record implements the generated With interface and gains copy methods:
@RecordBuilder
public record NameAndAge(String name, int age) implements NameAndAgeBuilder.With {}The record then exposes withName and withAge methods that return a new instance with one component changed. The idea is credited to Benji Weber. Alongside the individual withers, the interface supplies a with() method that hands you a builder seeded with the current values, so you can change several components in one call and finish with build().
The with() method also takes a lambda, which is the interesting part. Inside the lambda you can read a component through the getter and write another one, so you can express logic that depends on the current value, such as prefixing a name only when an age passes a threshold. The builder returned by the lambda variant does not need a call to build() before use, the README notes, since the wither produces the record for you.
If you cannot add the With interface to a record, for example because the record is generated by another tool or the interface would collide with something, the same behaviour is available statically: NameAndAgeBuilder.from(r5) returns a With implementation for an existing instance, so from(r5).withName("boop") works without the interface. That fallback is the reason the generated class has a from method at all.
Generating a record from an interface template
The third feature turns the generator around. An annotation generates a Java record from an Interface template, which is a different tool from the one most people expect in this space. Instead of describing a data class and asking for a builder, you describe a contract and get the record implementation.
The README lists this under Record From Interface Details alongside the generation via includes mechanism, which suggests the interface route has more than one way of being filled in. When an interface is the source of truth, the record's components, its accessors and its constructor come from the interface rather than from a hand-written declaration, and the generated type keeps the shape of the contract.
This matters in projects where the interface is already the stable public API and the record is an implementation detail. In that situation writing the record by hand duplicates the interface's shape, and any change to the interface has to be mirrored. Generating the record removes that duplication, and the annotation processor does it at compile time where the compiler can check that the generated type still matches the interface.
The README also points to details files for the parts it does not inline: customizing.md for options such as adding immutable collections, options.md for the switches, and deconstructors.md for the newer deconstructors feature.
Customization, options, includes and deconstructors
The processor is described as having only a few moving components but a lot of configurable parameters, and the documentation is split across several files rather than crammed into one page. customizing.md covers things such as adding immutable collections to the generated builder, which tells you the generated class is not fixed but parameterised. options.md holds the option switches, and the deconstructors.md page documents the newest addition, deconstructors, which the README marks as new.
The pattern of destructors is the mirror image of the withers. A wither copies a record and changes one component. A destructor does the opposite, producing a record with one component removed or unset, which is how you express a record that has a required part and an optional part when the type system insists on a single fixed shape.
Generation via includes is the other mechanism, and it controls which types are processed. For a large code base this is the switch that matters most in practice, because it lets a team adopt the processor incrementally instead of regenerating builders for every record in the repository at once.
One piece of advice from the usage notes is worth repeating, because it is the opposite of what people usually want. Set as few parameters as possible to begin with, since the more complicated the configuration gets, the more difficult troubleshooting can become. A generated builder with dozens of switches enabled is hard to reason about when something does not compile.
Getting the processor into your build
The artifact is on Maven Central under the group io.soabase.record-builder with the artifact id record-builder, which is what the badge in the README links to in the Maven Central search. Adding an annotation processor to a Maven build means declaring it as a dependency with the processor path configured, and the project itself is built with Maven: the repository ships pom.xml at the top level together with mvnw and mvnw.cmd, so the wrapper is available and a local Maven installation is not strictly necessary.
The repository layout shows how the project is split. record-builder-processor holds the processor itself, record-builder-core holds the core library, record-builder-validator is a separate validator module, record-builder-test holds the tests, and record-builder-lombok-tests is a module dedicated to running against Lombok. The existence of that last module says something specific: this processor is expected to coexist with other annotation processors in the same compilation, which is the situation that decides whether a generated builder survives contact with a real build.
There is also an enable-preview topic on the repository. That is a reminder that some of the functionality here, notably the record generation from an interface, may rely on preview language features, which is exactly the kind of thing to check against your own build flags before committing to it.
Generated builders against hand-written ones
The alternative to RecordBuilder is the obvious one: write the builder by hand. For a record with three components that is a short class, and you get exactly the API you want, including validation in the constructor or a builder method that refuses invalid states. The cost is one class per record, maintained by hand, and it never shrinks as the number of record types in a service grows.
A second alternative is the general-purpose builder library that already exists in the Java ecosystem, with Lombok as the obvious comparison since the repository has a dedicated test module for it. The difference in approach is that a general-purpose builder library is a runtime dependency with reflection or bytecode tricks, whereas an annotation processor writes plain Java source at compile time. That difference shows up in your build: annotation processing is a compile step you configure, and it can conflict with another processor, while a runtime library is just a dependency.
RecordBuilder's specific niche is the wither. A generic builder library does not give you withName and withAge methods on the record itself, so a copy with one field changed means going through the builder and back again. If copy-with-one-field-changed is a pattern in your code, that pattern is the reason to look at this processor rather than at a general builder library.
Editorial conclusion
RecordBuilder suits Java teams working with records on Java 17 and above who want builder and wither ergonomics without hand-writing boilerplate per type, and it does not suit projects that have decided to avoid annotation processing or that need behaviour the generated class cannot express. Before adopting it, check that your build runs annotation processors, read options.md for the switches that change what is generated, and remember that the generated builder is a concrete class with its own equals, hashCode and toString, so it does not behave like an immutable value type in every place.
Frequently asked questions
What does the RecordBuilder annotation processor generate?
It generates a companion builder class for a Java record, an interface that adds with copy methods to that record, and, through a separate annotation, a Java record generated from an interface template. The builder exposes builder(), builder(from), a static constructor, a stream of components, per-component setters and getters, and build().
How do I add withers to a Java record with RecordBuilder?
Have the record implement the generated With interface, for example public record NameAndAge(String name, int age) implements NameAndAgeBuilder.With. That adds withName and withAge methods, and if you cannot change the record you can use the static NameAndAgeBuilder.from(instance) instead.
Which Java versions does RecordBuilder build against?
The README shows separate Maven build badges for Java 17, Java 21 and Java 24, and the project topics include java-16 and java-17 because records arrived in Java 16. The library is published on Maven Central as io.soabase.record-builder:record-builder.
Can RecordBuilder generate a record from an interface?
Yes. One of the three things the processor creates is an annotation that generates a Java record from an interface template, which is documented separately from the builder and wither features. The README also describes a generation via includes mechanism for controlling what gets processed.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/randgalt-record-builder)Star history chart for your README
[](https://hysenlabs.com/projects/randgalt-record-builder)Was this useful?