Building on the foundation of clean record validation
Java Records transformed how we handle immutable data in Java, but they left us with one challenge: creating builder patterns for complex object construction. While Records are perfect for simple data holders, real-world applications often need the flexibility of the builder pattern.
In my previous article about Record constructor validation, I explored how to keep validation logic clean and maintainable. Today, I want to introduce Record Companion - a library that brings the same philosophy of simplicity to builder pattern generation.
The Builder Pattern Problem
Let's say you have a User record:
public record User(String name, int age, String email) {}
Creating instances with all parameters is straightforward:
User user = new User("John Doe", 30, "john@example.com");
But what happens when you need:
- Optional parameters
- Step-by-step construction
- Functional updates
- Immutable modifications
You'd typically need to write a lot of boilerplate code. Record Companion solves this with a single annotation.
Enter Record Companion
Record Companion follows the same philosophy I outlined in the validation article: the goal isn't to have the most features—it's to have code that's easy to read, maintain, and debug.
Here's how simple it is:
import io.github.recordcompanion.annotations.Builder;
@Builder
public record User(String name, int age, String email) {}
That's it. One annotation, and you get:
// Create new instances
User user = UserBuilder.builder()
.name("John Doe")
.age(30)
.email("john@example.com")
.build();
// Create from existing record
User updatedUser = UserBuilder.builder(user)
.age(31)
.build();
// Functional updates
User modifiedUser = UserBuilder.with(user, updater ->
updater.name("Jane Doe").email("jane@example.com")
);
What Record Companion Generates
For each @Builder
annotated record, you get two components:
1. Builder Class
A concrete implementation with fluent methods:
public final class UserBuilder implements UserUpdater {
public UserBuilder name(String name) { ...}
public UserBuilder age(int age) { ...}
public UserBuilder email(String email) { ...}
public User build() { ...}
// Static factory methods
public static UserBuilder builder() { ...}
public static UserBuilder builder(User existing) { ...}
public static User with(User existing, Consumer<UserUpdater> updater) { ...}
}
2. Updater Interface
For flexible composition and functional updates:
public interface UserUpdater {
UserUpdater name(String name);
UserUpdater age(int age);
UserUpdater email(String email);
}
Advanced Features (Still Simple)
Generic Types
Record Companion handles complex generics naturally:
@Builder
public record Container<T, U extends Number>(
String name,
List<T> items,
Map<String, U> values
) {}
// Usage with proper type inference
Container<String, Integer> container =
ContainerBuilder.<String, Integer>builder()
.name("My Container")
.items(List.of("item1", "item2"))
.values(Map.of("count", 42))
.build();
Annotation Copying
For framework integration (like Bean Validation):
@Builder(copyAnnotations = true)
public record ValidatedUser(
@NotNull @Size(min = 1, max = 100) String name,
@Min(0) @Max(150) int age,
@Email String email
) {}
// Generated methods preserve annotations:
// @NotNull @Size(min = 1, max = 100)
// public ValidatedUserBuilder name(String name) { ... }
Bean Validation Integration
Record Companion provides seamless integration with Bean Validation annotations through automatic ValidCheck code generation.
Automatic Validation Code Generation
When you combine @Builder
and @ValidCheck
with Bean Validation annotations, Record Companion automatically generates validation code:
@Builder
@ValidCheck
public record UserProfile(
@NotNull @Size(min = 3, max = 20) String username,
@Min(0) @Max(100) int score,
@NotEmpty Map<String, String> metadata,
@Pattern(regexp = "[A-Z]{3}[0-9]{3}") String code
) {
public UserProfile {
// Automatic validation using generated check class
UserProfileCheck.validate(username, score, metadata, code);
}
}
Comprehensive Bean Validation Support
Record Companion maps Bean Validation annotations directly to ValidCheck API calls:
-
@NotNull
→.notNull(value, fieldName)
-
@NotEmpty
→.notNullOrEmpty(value, fieldName)
-
@Size(min, max)
→.hasLength(value, min, max, fieldName)
-
@Pattern(regexp)
→.matches(value, pattern, fieldName)
-
@Min + @Max
(combined) →.inRange(value, min, max, fieldName)
Generated Validation Classes
For each record with @ValidCheck
, you get three convenient validation methods:
// BatchValidator for manual control
UserProfileCheck.check(username, score, metadata, code)
// Validator for immediate validation with chaining
UserProfileCheck.require(username, score, metadata, code)
// Convenience method - validates and throws on failure
UserProfileCheck.validate(username, score, metadata, code)
Generated Validation Chain Example
Here's what the generated validation looks like:
return validator
.notNull(username, "username")
.hasLength(username, 3, 20, "username")
.inRange(score, 0, 100, "score")
.notNullOrEmpty(metadata, "metadata")
.matches(code, "[A-Z]{3}[0-9]{3}", "code");
Builder Pattern with Automatic Validation
When you use the builder pattern, validation happens automatically:
// Builder pattern with automatic validation
UserProfile profile = UserProfileBuilder.builder()
.username("john_doe")
.score(85)
.metadata(Map.of("role", "admin"))
.code("ABC123")
.build(); // Validation happens automatically
// Functional updates with validation
UserProfile updated = UserProfileBuilder.with(profile, updater ->
updater.score(95)
); // Re-validates with new values
Getting Started
Add Record Companion to your project:
Maven:
<!-- Record Companion Processor -->
<dependency>
<groupId>io.github.record-companion</groupId>
<artifactId>record-companion-processor</artifactId>
<version>0.1.1</version>
<scope>provided</scope>
</dependency>
<!-- ValidCheck library (required only for @ValidCheck integration) -->
<dependency>
<groupId>io.github.validcheck</groupId>
<artifactId>validcheck</artifactId>
<version>0.9.3</version>
</dependency>
<!-- Bean Validation API (required only for @ValidCheck integration) -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
<scope>provided</scope>
</dependency>
Gradle:
// Record Companion Processor
annotationProcessor 'io.github.record-companion:record-companion-processor:0.1.1'
compileOnly 'io.github.record-companion:record-companion-processor:0.1.1'
// ValidCheck library (required only for @ValidCheck integration)
implementation 'io.github.validcheck:validcheck:0.9.3'
// Bean Validation API (required only for @ValidCheck integration)
compileOnly 'javax.validation:validation-api:2.0.1.Final'
That's it. No configuration files, no complex setup.
Requirements
- Java 17+ (for record support)
- Maven 3.6+ or Gradle 6.0+
- Any IDE (no special plugins required)
Alternatives
If you're looking for more features, record-builder is a popular and well-established alternative. It offers more advanced features like "wither" methods and interface-based record generation.
Record Companion focuses on simplicity and ease of use, making it perfect if you just want straightforward builder patterns without the extra bells and whistles.
Conclusion
Java Records gave us immutable data structures. ValidCheck gave us clean validation. Record Companion completes the picture with simple, powerful builder patterns and seamless Bean Validation integration.
The combination of these three - Records + ValidCheck + Record Companion - creates a development experience that's both powerful and maintainable. You get immutability, declarative validation through standard Bean Validation annotations, and flexible construction without drowning in boilerplate.
The automatic Bean Validation mapping means you can use industry-standard validation annotations while still benefiting from ValidCheck's clean API and Record Companion's builder pattern generation.
Try Record Companion in your next project. Your future self (and your teammates) will thank you for choosing simplicity with power.
Resources:
What's your experience with builder patterns in Java? Have you tried Record Companion's Bean Validation features? Share your thoughts in the comments below!
Top comments (0)