google-site-verification: google61fe8ba583a51912.html
Boilerplate-Free Java: Mastering Immutables.org Code Generation

Immutables.org is a Java annotation processor that generates immutable objects, builders, and utility methods, eliminating the verbose boilerplate typically associated with writing immutable value classes in Java. By generating code at compile time, it provides the benefits of immutability with minimal developer effort.

Why Immutables.org?

Traditional Java Value Class:

// 50+ lines of boilerplate public final class Person { private final String name; private final int age; private final List<String> emails; public Person(String name, int age, List<String> emails) { this.name = name; this.age = age; this.emails = Collections.unmodifiableList(new ArrayList<>(emails)); } // Getters, equals, hashCode, toString... // Builder class would be even more code }

With Immutables.org:

// Just 5 lines! @Value.Immutable public interface Person { String name(); int age(); List<String> emails(); }

Setup and Configuration

1. Maven Dependencies

<dependencies> <dependency> <groupId>org.immutables</groupId> <artifactId>value</artifactId> <version>2.9.3</version> <scope>provided</scope> </dependency> </dependencies>

2. Basic Annotation Processor Setup

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <annotationProcessorPaths> <path> <groupId>org.immutables</groupId> <artifactId>value</artifactId> <version>2.9.3</version> </path> </annotationProcessorPaths> <compilerArgs> <arg>-Amapstruct.defaultComponentModel=spring</arg> </compilerArgs> </configuration> </plugin> </plugins> </build>

Core Immutables Usage Patterns

1. Basic Immutable Objects

import org.immutables.value.Value; @Value.Immutable public interface User { String username(); String email(); int age(); boolean active(); } // Generated class: ImmutableUser // Usage: public class UserService { public void demonstrateUsage() { // Builder pattern User user = ImmutableUser.builder() .username("john_doe") .email("[email protected]") .age(30) .active(true) .build(); // Copy with modifications User updatedUser = ImmutableUser.builder() .from(user) .age(31) .build(); System.out.println(user.username()); // "john_doe" System.out.println(user.equals(updatedUser)); // false } }

2. Collections and Default Values

import java.util.List; import java.util.Set; import java.util.Map; @Value.Immutable public abstract class Product { public abstract String sku(); public abstract String name(); public abstract List<String> categories(); public abstract Set<String> tags(); public abstract Map<String, String> attributes(); @Value.Default public boolean available() { return true; } @Value.Default public int stock() { return 0; } @Value.Derived public String displayName() { return name() + " (" + sku() + ")"; } } // Usage example public class ProductDemo { public void demonstrate() { Product product = ImmutableProduct.builder() .sku("PROD-001") .name("Laptop") .addCategories("Electronics", "Computers") .addTags("new", "featured") .putAttributes("color", "black") .putAttributes("weight", "2.5kg") .stock(50) .build(); System.out.println(product.displayName()); // "Laptop (PROD-001)" System.out.println(product.available()); // true (default) } }

3. Optional and Nullable Fields

import java.util.Optional; @Value.Immutable public interface Customer { String id(); String name(); Optional<String> phoneNumber(); Optional<String> secondaryEmail(); @Value.Auxiliary Optional<String> internalNotes(); } // Usage with Optional fields public class CustomerService { public void handleCustomer() { // Customer with phone number Customer withPhone = ImmutableCustomer.builder() .id("CUST-001") .name("Alice Smith") .phoneNumber("+1234567890") .build(); // Customer without phone number Customer withoutPhone = ImmutableCustomer.builder() .id("CUST-002") .name("Bob Johnson") .build(); // Safe access to optional fields withPhone.phoneNumber().ifPresent(phone -> System.out.println("Phone: " + phone)); // Default value for absent optional String contact = withoutPhone.phoneNumber() .orElse("No phone provided"); } }

Advanced Features

1. JSON Serialization with Jackson

import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @Value.Immutable @JsonSerialize(as = ImmutableOrder.class) @JsonDeserialize(as = ImmutableOrder.class) public interface Order { String orderId(); String customerId(); List<OrderItem> items(); OrderStatus status(); @Value.Immutable @JsonSerialize(as = ImmutableOrderItem.class) @JsonDeserialize(as = ImmutableOrderItem.class) interface OrderItem { String productId(); int quantity(); double price(); } enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED } } // JSON Serialization usage public class OrderService { private final ObjectMapper mapper = new ObjectMapper(); public void jsonDemo() throws Exception { Order order = ImmutableOrder.builder() .orderId("ORD-001") .customerId("CUST-001") .addItems(ImmutableOrderItem.builder() .productId("PROD-001") .quantity(2) .price(299.99) .build()) .status(Order.OrderStatus.PENDING) .build(); // Serialize to JSON String json = mapper.writeValueAsString(order); System.out.println(json); // Deserialize from JSON Order deserialized = mapper.readValue(json, Order.class); System.out.println(deserialized.orderId()); // "ORD-001" } }

2. Custom Builder Methods

@Value.Immutable @Value.Style(strictBuilder = true) public abstract class BankAccount { public abstract String accountNumber(); public abstract String owner(); public abstract double balance(); public abstract Currency currency(); // Custom builder method public static ImmutableBankAccount.Builder builder() { return ImmutableBankAccount.builder(); } // Factory method with validation public static BankAccount create(String accountNumber, String owner, double initialBalance, Currency currency) { if (initialBalance < 0) { throw new IllegalArgumentException("Initial balance cannot be negative"); } return ImmutableBankAccount.builder() .accountNumber(accountNumber) .owner(owner) .balance(initialBalance) .currency(currency) .build(); } // Instance methods public BankAccount deposit(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Deposit amount must be positive"); } return ImmutableBankAccount.builder() .from(this) .balance(this.balance() + amount) .build(); } public BankAccount withdraw(double amount) { if (amount <= 0) { throw new IllegalArgumentException("Withdrawal amount must be positive"); } if (amount > this.balance()) { throw new IllegalArgumentException("Insufficient funds"); } return ImmutableBankAccount.builder() .from(this) .balance(this.balance() - amount) .build(); } } // Usage with custom methods public class BankDemo { public void demonstrate() { BankAccount account = BankAccount.create("ACC-123", "John Doe", 1000.0, Currency.USD); BankAccount afterDeposit = account.deposit(500.0); BankAccount afterWithdrawal = afterDeposit.withdraw(200.0); System.out.println("Final balance: " + afterWithdrawal.balance()); // 1300.0 } }

3. Nested Immutable Structures

@Value.Immutable public abstract class Project { public abstract String id(); public abstract String name(); public abstract List<Task> tasks(); public abstract Team team(); @Value.Immutable public abstract static class Task { public abstract String taskId(); public abstract String description(); public abstract Optional<LocalDate> dueDate(); public abstract TaskStatus status(); public abstract Set<String> assignees(); public enum TaskStatus { TODO, IN_PROGRESS, REVIEW, DONE } } @Value.Immutable public abstract static class Team { public abstract String teamId(); public abstract String name(); public abstract List<TeamMember> members(); } @Value.Immutable public abstract static class TeamMember { public abstract String memberId(); public abstract String name(); public abstract String role(); } } // Complex nested builder public class ProjectManager { public Project createProject() { return ImmutableProject.builder() .id("PROJ-001") .name("New Website") .addTasks(ImmutableTask.builder() .taskId("TASK-001") .description("Design homepage") .dueDate(LocalDate.of(2024, 12, 31)) .status(Project.Task.TaskStatus.TODO) .addAssignees("DES-001") .build()) .team(ImmutableTeam.builder() .teamId("TEAM-001") .name("Web Team") .addMembers(ImmutableTeamMember.builder() .memberId("DES-001") .name("Alice") .role("Designer") .build()) .build()) .build(); } }

Style Customization and Configuration

1. Global Style Configuration

// Maven compiler configuration for global styles <compilerArgs> <arg>-Aimmutables.generated=.*</arg> <arg>-Aimmutables.style=strict</arg> <arg>-Aimmutables.auxiliary=.*</arg> </compilerArgs>

2. Annotation-Based Style Configuration

import org.immutables.value.Value.Style; // Custom style for different naming conventions @Style( typeImmutable = "*", // Generate Immutable* for all types typeAbstract = "*", // Accept any abstract type visibility = Style.ImplementationVisibility.PUBLIC, builder = "new", // Use 'new' instead of 'builder' build = "create", // Use 'create' instead of 'build' defaults = @Value.Immutable(builder = false) // Disable builder by default ) public @interface CustomStyle {} @CustomStyle @Value.Immutable public interface Configuration { String host(); int port(); boolean sslEnabled(); } // Usage with custom style public class ConfigDemo { public void demonstrate() { // Now uses 'new' and 'create' instead of 'builder' and 'build' Configuration config = new ImmutableConfiguration() .host("localhost") .port(8080) .sslEnabled(false) .create(); } }

3. Different Styles for Different Use Cases

// Jackson-friendly style @Style( typeImmutable = "*", visibility = Style.ImplementationVisibility.PACKAGE, overshadowImplementation = true, builderVisibility = Style.ImplementationVisibility.PACKAGE ) @interface JacksonStyle {} // MongoDB-friendly style @Style( typeImmutable = "Mongo*", passAnnotations = { org.mongodb.morphia.annotations.Entity.class } ) @interface MongoStyle {} @JacksonStyle @Value.Immutable public interface ApiResponse { boolean success(); String message(); Object data(); } @MongoStyle @Value.Immutable @Entity("users") public abstract class MongoUser { @org.mongodb.morphia.annotations.Id public abstract String id(); public abstract String username(); public abstract String email(); }

Validation and Preconditions

1. Attribute Validation

@Value.Immutable @Value.Style(strictBuilder = true) public abstract class ValidatedUser { @Value.Check protected void check() { if (username().length() < 3) { throw new IllegalStateException("Username must be at least 3 characters"); } if (age() < 0 || age() > 150) { throw new IllegalStateException("Age must be between 0 and 150"); } } public abstract String username(); public abstract int age(); public abstract List<String> roles(); // Individual attribute validation @Value.Check protected void validateRoles() { if (roles().isEmpty()) { throw new IllegalStateException("User must have at least one role"); } } } // Usage with validation public class ValidationDemo { public void demonstrate() { try { ImmutableValidatedUser.builder() .username("ab") // Too short - will fail .age(25) .addRoles("USER") .build(); } catch (IllegalStateException e) { System.out.println("Validation failed: " + e.getMessage()); } } }

2. Precondition Methods

@Value.Immutable @Value.Style(strictBuilder = true) public abstract class InventoryItem { public abstract String sku(); public abstract String name(); public abstract int quantity(); public abstract double price(); // Precondition methods in builder public static class Builder extends ImmutableInventoryItem.Builder { @Override public InventoryItem build() { // Custom validation logic if (quantity < 0) { throw new IllegalStateException("Quantity cannot be negative"); } if (price <= 0) { throw new IllegalStateException("Price must be positive"); } return super.build(); } } }

Integration with Other Libraries

1. Spring Boot Configuration

@Value.Immutable @JsonSerialize(as = ImmutableAppConfig.class) @JsonDeserialize(as = ImmutableAppConfig.class) public interface AppConfig { String databaseUrl(); String databaseUsername(); String databasePassword(); int serverPort(); boolean debugEnabled(); @Value.Default default int cacheSize() { return 1000; } } @Component @ConfigurationProperties(prefix = "app") @EnableConfigurationProperties public class ApplicationConfiguration { private AppConfig config; public void setConfig(AppConfig config) { this.config = config; } public AppConfig getConfig() { return config; } } // application.yml app: config: database-url: "jdbc:postgresql://localhost:5432/mydb" database-username: "admin" database-password: "secret" server-port: 8080 debug-enabled: true cache-size: 5000

2. JPA Entity Projections

// Immutable projection for JPA queries @Value.Immutable public interface UserProjection { String getUsername(); String getEmail(); LocalDateTime getCreatedAt(); // Static factory method from entity static UserProjection from(UserEntity entity) { return ImmutableUserProjection.builder() .username(entity.getUsername()) .email(entity.getEmail()) .createdAt(entity.getCreatedAt()) .build(); } } // JPA Repository usage @Repository public interface UserRepository extends JpaRepository<UserEntity, Long> { @Query("SELECT new package.ImmutableUserProjection(u.username, u.email, u.createdAt) " + "FROM UserEntity u WHERE u.active = true") List<UserProjection> findActiveUserProjections(); }

Testing with Immutables

1. Test Data Builders

@Value.Immutable public interface TestUser { String username(); String email(); int age(); List<String> permissions(); // Test data builder pattern class Builder extends ImmutableTestUser.Builder { public Builder withDefaultValues() { return this .username("testuser") .email("[email protected]") .age(25) .addPermissions("READ", "WRITE"); } public Builder admin() { return withDefaultValues() .username("admin") .addPermissions("ADMIN"); } } } // Usage in tests public class UserServiceTest { @Test public void testUserCreation() { TestUser user = new TestUser.Builder() .withDefaultValues() .username("specific_user") .build(); assertThat(user.username()).isEqualTo("specific_user"); assertThat(user.permissions()).contains("READ", "WRITE"); } @Test public void testAdminUser() { TestUser admin = new TestUser.Builder() .admin() .build(); assertThat(admin.permissions()).contains("ADMIN"); } }

2. JSON Testing

public class JsonSerializationTest { private final ObjectMapper mapper = new ObjectMapper(); @Test public void testSerializationRoundTrip() throws Exception { TestUser original = ImmutableTestUser.builder() .username("testuser") .email("[email protected]") .age(30) .addPermissions("READ") .build(); String json = mapper.writeValueAsString(original); TestUser deserialized = mapper.readValue(json, TestUser.class); assertThat(deserialized).isEqualTo(original); } }

Performance Considerations

1. Lazy Derived Attributes

@Value.Immutable public abstract class ExpensiveComputation { public abstract String input(); @Value.Lazy public String expensiveResult() { // This computation is cached after first call try { Thread.sleep(1000); // Simulate expensive operation return input().toUpperCase(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException(e); } } } // Usage public class PerformanceDemo { public void demonstrate() { ExpensiveComputation comp = ImmutableExpensiveComputation.of("hello"); // First call computes and caches long start1 = System.currentTimeMillis(); String result1 = comp.expensiveResult(); long time1 = System.currentTimeMillis() - start1; // Second call uses cached value long start2 = System.currentTimeMillis(); String result2 = comp.expensiveResult(); long time2 = System.currentTimeMillis() - start2; System.out.println("First call: " + time1 + "ms"); // ~1000ms System.out.println("Second call: " + time2 + "ms"); // ~0ms } }

Best Practices and Pitfalls

1. Do's and Don'ts

// ✅ GOOD: Simple, focused interfaces @Value.Immutable public interface GoodExample { String name(); int value(); Optional<String> description(); } // ❌ BAD: Too many responsibilities @Value.Immutable public interface BadExample { String name(); int value(); String description(); List<String> tags(); Map<String, String> metadata(); LocalDateTime created(); LocalDateTime updated(); // ... 20 more fields } // ✅ GOOD: Use builders for complex objects @Value.Immutable public abstract class ComplexButManageable { public abstract String id(); public abstract Configuration config(); public abstract List<Item> items(); @Value.Immutable public abstract static class Configuration { public abstract String host(); public abstract int port(); public abstract boolean ssl(); } } // ✅ GOOD: Use default values appropriately @Value.Immutable public interface SmartDefaults { String name(); @Value.Default default boolean enabled() { return true; } @Value.Default default int maxRetries() { return 3; } @Value.Default default Duration timeout() { return Duration.ofSeconds(30); } }

Migration Strategy

1. Gradual Migration from Lombok

// Phase 1: Coexistence // lombok.config lombok.anyConstructor.suppressConstructorProperties = true // Existing Lombok class @Data @Builder @AllArgsConstructor public class LegacyUser { private String username; private String email; private int age; } // New Immutables interface @Value.Immutable public interface NewUser { String username(); String email(); int age(); // Conversion method static NewUser from(LegacyUser legacy) { return ImmutableNewUser.builder() .username(legacy.getUsername()) .email(legacy.getEmail()) .age(legacy.getAge()) .build(); } LegacyUser toLegacy() { return LegacyUser.builder() .username(username()) .email(email()) .age(age()) .build(); } }

Conclusion

Immutables.org provides significant advantages for Java development:

Key Benefits:

  • Reduced Boilerplate: 90% less code for value objects
  • Compile-time Safety: All code generation happens at compile time
  • Runtime Performance: No reflection overhead
  • Rich Feature Set: Builders, JSON, validation, and more
  • Framework Integration: Works with Spring, Jackson, JPA, etc.

When to Use:

  • Configuration objects
  • DTOs and API models
  • Value objects in domain-driven design
  • Immutable data structures
  • Projections and view models

When to Consider Alternatives:

  • Extremely simple data holders (record classes in Java 14+)
  • Cases requiring runtime bytecode generation
  • Projects already heavily invested in other code generation tools

Immutables.org strikes an excellent balance between convenience and control, making it an ideal choice for modern Java applications that value immutability, type safety, and maintainability.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper