DEV Community

Alexey Gavrilov
Alexey Gavrilov

Posted on • Edited on

Record Companion: Simple Builder Pattern for Java Records

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) {}
Enter fullscreen mode Exit fullscreen mode

Creating instances with all parameters is straightforward:

User user = new User("John Doe", 30, "john@example.com");
Enter fullscreen mode Exit fullscreen mode

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) {}
Enter fullscreen mode Exit fullscreen mode

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")
);
Enter fullscreen mode Exit fullscreen mode

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) { ...}
}
Enter fullscreen mode Exit fullscreen mode

2. Updater Interface

For flexible composition and functional updates:

public interface UserUpdater {
    UserUpdater name(String name);
    UserUpdater age(int age);
    UserUpdater email(String email);
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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) { ... }
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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)