Question 4 · Section 20

Can you add additional methods to a Record

Record allows adding: 4. Private methods (for internal logic) 5. Constructors (must call canonical)

Language versions: English Russian Ukrainian

🟢 Junior Level

Yes, you can! A Record is not just a set of fields. You can add any methods, but with some restrictions.

What you can:

public record Money(BigDecimal amount, String currency) {
    // ✅ Static methods
    public static Money zero() {
        return new Money(BigDecimal.ZERO, "USD");
    }

    // ✅ Additional methods
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException("Different currencies");
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }

    // ✅ Private methods (Java 16+)
    private void validate() {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Negative amount");
        }
    }
}

What you cannot:

  • ❌ Add instance fields (except static)
  • ❌ Make Record mutable

🟡 Middle Level

How it works

Record allows adding:

  1. Static fields (static fields)
  2. Static methods (static methods)
  3. Instance methods (any visibility)
  4. Private methods (for internal logic)
  5. Constructors (must call canonical)

Examples:

public record User(String name, int age, String email) {
    // ✅ Static field
    public static final int MAX_AGE = 150;

    // ✅ Static factory
    public static User anonymous() {
        return new User("Anonymous", 0, "");
    }

    // ✅ Instance method
    public boolean isAdult() {
        return age >= 18;
    }

    // ✅ Private method
    private boolean isValidEmail() {
        return email != null && email.contains("@");
    }

    // ✅ Canonical constructor with validation
    public User {
        if (age < 0 || age > MAX_AGE) {
            throw new IllegalArgumentException("Invalid age");
        }
    }
}

Common Mistakes

  1. Trying to add instance field:
    public record BadRecord() {
     // ❌ Instance field — compilation error
     private int mutableField = 0;
    
     // ✅ Only static
     private static int counter = 0;
    }
    
  2. Changing accessor return type:
    public record Point(int x, int y) {
     // ❌ Cannot override accessor with different type
     public String x() {  // compilation error
         return String.valueOf(x);
     }
    }
    

Practical Application

1. Value objects with logic:

public record Range(int min, int max) {
    public Range {
        if (min > max) {
            throw new IllegalArgumentException("min > max");
        }
    }

    public boolean contains(int value) {
        return value >= min && value <= max;
    }

    public Range intersect(Range other) {
        return new Range(
            Math.max(this.min, other.min),
            Math.min(this.max, other.max)
        );
    }
}

2. Builder-like methods:

public record Query(String table, List<String> columns, String where) {
    public Query(String table) {
        this(table, List.of("*"), null);
    }

    public Query select(String... columns) {
        return new Query(this.table, List.of(columns), this.where);
    }

    public Query where(String condition) {
        return new Query(this.table, this.columns, condition);
    }
}

// Usage
Query q = new Query("users")
    .select("name", "email")
    .where("age > 18");

🔴 Senior Level

Internal Implementation

Compiler allows:

  • Additional methods do not affect canonical constructor
  • Instance methods do not change Record structure
  • Static fields are stored in the class, not in instances

Override restrictions:

public record User(String name) {
    // Starting from Java 16+ you CAN override accessor with the same signature.
    // In preview versions this was forbidden.
    public String name() { return name.toUpperCase(); }  // OK in Java 16+

    // ❌ Cannot override final methods from java.lang.Record
    // Methods equals/hashCode/toString are declared as abstract in java.lang.Record,
    // but you CAN and MUST override them.
}

Architectural Trade-offs

Methods in Record vs separate utilities:

Approach Pros Cons
Methods in Record Logic next to data, more convenient Record becomes “fatter”
Separate utilities Record stays simple More files, imports

Edge Cases

1. Overriding equals/hashCode:

public record CaseInsensitiveString(String value) {
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof CaseInsensitiveString other)) return false;
        return Objects.equals(
            this.value.toLowerCase(),
            other.value.toLowerCase()
        );
    }

    @Override
    public int hashCode() {
        return value.toLowerCase().hashCode();
    }
}

2. Private constructors:

public record UserId(UUID value) {
    // ✅ Private constructor (must call canonical)
    private UserId(String value) {
        this(UUID.fromString(value));
    }

    public static UserId of(String value) {
        return new UserId(value);
    }
}

3. Generic methods in Record:

public record JsonNode(String json) {
    public <T> T parse(Class<T> type) {
        // JSON parsing
        return null;
    }
}

Performance

Additional methods:
- No memory overhead (methods are in class, not instance)
- JIT inlines methods as usual
- No difference from regular class

Production Experience

Real-world example — Domain Events:

public record OrderCreatedEvent(
    String orderId,
    List<OrderItem> items,
    Instant createdAt
) {
    public BigDecimal totalAmount() {
        return items.stream()
            .map(item -> item.price().multiply(BigDecimal.valueOf(item.quantity())))
            .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public boolean isHighValue() {
        return totalAmount().compareTo(new BigDecimal("1000")) > 0;
    }

    public record OrderItem(String productId, int quantity, BigDecimal price) {}
}

Best Practices

// ✅ Add business logic to Record
public record Money(BigDecimal amount, Currency currency) {
    public Money add(Money other) { /* ... */ }
    public boolean isPositive() { return amount.compareTo(BigDecimal.ZERO) > 0; }
}

// ✅ Static factories for convenience
public record Point(double x, double y) {
    public static Point fromPolar(double r, double theta) {
        return new Point(r * Math.cos(theta), r * Math.sin(theta));
    }
}

// ❌ Don't make Record too "fat"
// ❌ Don't add mutable state
// ❌ Don't override accessors without reason

🎯 Interview Cheat Sheet

Must know:

  • Record can have static fields, static methods, instance methods
  • Instance fields (non-static) — forbidden, only final canonical components
  • Private methods are supported (Java 16+)
  • Constructors must call canonical via this(...)
  • You can override equals/hashCode/toString, but it’s not mandatory
  • Accessors can be overridden in Java 16+ (forbidden in preview)

Common follow-up questions:

  • Can you add a field to Record? — Only static, instance fields are forbidden
  • Can you add a private method? — Yes, private methods are fully supported
  • Can you overload constructors? — Yes, but additional constructor must call canonical
  • Should you override equals/hashCode? — Only if custom logic is needed (case-insensitive, etc.)

Red flags (DO NOT say):

  • ❌ “You cannot add any methods to Record” — You can add static and instance methods
  • ❌ “You can add mutable field to Record” — Only static fields, instance are forbidden
  • ❌ “Methods in Record increase object memory” — Methods are in class, not instance
  • ❌ “Cannot override accessor” — Can in Java 16+, but with same signature

Related topics:

  • [[1. What is Record in Java and since which version are they available]]
  • [[5. What methods are automatically generated for a Record]]
  • [[6. Can you override constructor in Record]]
  • [[8. Can you declare static fields and methods in Record]]