What is compact constructor in Record
It doesn't contain a signature — the compiler understands it's a compact constructor on its own.
🟢 Junior Level
Compact constructor is a special form of constructor in Record that is used only for validation and normalization of data.
It doesn’t contain a signature — the compiler understands it’s a compact constructor on its own.
public record User(String name, int age) {
// Compact constructor — body only, no signature
public User {
if (age < 0) {
throw new IllegalArgumentException("Age cannot be negative");
}
}
}
Differences from regular constructor:
// Regular constructor
public User(String name, int age) {
this.name = name;
this.age = age;
}
// Compact constructor (validation only)
public User {
if (age < 0) {
throw new IllegalArgumentException();
}
}
🟡 Middle Level
How it works
Compact constructor is inlined into the canonical constructor automatically:
public record Email(String value) {
public Email {
// This code will execute BEFORE field assignment
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
}
}
// Compiler generates:
public Email(String value) {
// Code from compact constructor
if (value == null || !value.contains("@")) {
throw new IllegalArgumentException("Invalid email");
}
// Then assignment
this.value = value;
}
Important: Parameters in compact constructor are not final — you can change the value before assignment!
public record Email(String value) {
public Email {
value = value.toLowerCase().trim(); // ✅ normalization
}
}
Common Mistakes
- Trying to assign
this.value:public record User(String name) { public User { // ❌ Cannot use this.value this.name = name.toUpperCase(); // compilation error // ✅ Use plain name name = name.toUpperCase(); // OK — this is a parameter, not a field } } - Trying to add fields:
public record User() { public User { // ❌ Cannot add a field int extra = 0; // compilation error } }
Practical Application
1. Validation:
public record Money(BigDecimal amount) {
public Money {
Objects.requireNonNull(amount, "Amount cannot be null");
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Negative amount");
}
}
}
2. Normalization:
public record PhoneNumber(String value) {
public PhoneNumber {
// Remove everything except digits
value = value.replaceAll("[^0-9+]", "");
}
}
3. Normalization with mutable objects:
public record TagCloud(List<String> tags) {
public TagCloud {
// Defensive copy
tags = new ArrayList<>(tags);
// Normalization
tags.replaceAll(String::toLowerCase);
tags.sort(null);
}
}
🔴 Senior Level
Internal Implementation
Desugaring:
// Source code
public record User(String name, int age) {
public User {
if (age < 0) throw new IllegalArgumentException();
name = name.toUpperCase();
}
}
// Desugared code
public final class User extends Record {
private final String name;
private final int age;
public User(String name, int age) {
// Code from compact constructor
if (age < 0) throw new IllegalArgumentException();
name = name.toUpperCase();
// Field assignment (generated by compiler)
this.name = name;
this.age = age;
}
}
Restrictions:
- Compact constructor parameters are effectively final, but not final
- You can modify parameters before assignment
- Cannot access
thisbefore assignment // Since the canonical constructor signature doesn’t contain throws, // checked exceptions must be wrapped in unchecked. - Cannot throw checked exceptions (because canonical constructor doesn’t declare throws)
Architectural Trade-offs
Compact constructor vs full constructor:
| Aspect | Compact | Full |
|---|---|---|
| Validation | ✅ Ideal | ✅ Works |
| Normalization | ✅ Works | ✅ Works |
| Parameter modification | ✅ Allowed | ✅ Allowed |
| Delegation | ❌ Cannot | ✅ Can |
| Multiple constructors | ❌ One | ✅ Several |
Edge Cases
1. Normalization of mutable objects:
public record DateRange(LocalDate start, LocalDate end) {
public DateRange {
Objects.requireNonNull(start);
Objects.requireNonNull(end);
if (start.isAfter(end)) {
// Auto-correction
var temp = start;
start = end;
end = temp;
}
}
}
2. Compact constructor with checked exception:
public record JsonData(String json) {
public JsonData {
try {
Json.parse(json);
} catch (JsonParseException e) {
// ❌ Cannot throw checked exception
// ✅ Wrap in unchecked
throw new IllegalArgumentException("Invalid JSON", e);
}
}
}
3. Compact constructor + additional constructor:
public record Point(int x, int y) {
// Compact — for validation
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException("Negative coordinates");
}
}
// Additional — for convenience
public Point(int value) {
this(value, value); // calls canonical (with validation)
}
}
Performance
Compact constructor:
- Zero overhead — code is inlined into canonical constructor
- JIT can optimize validation
- No additional calls
Production Experience
Value objects with invariants:
public record Range(int min, int max) {
public Range {
if (min > max) {
throw new IllegalArgumentException(
"Min (%d) cannot be greater than max (%d)".formatted(min, max));
}
}
public boolean contains(int value) {
return value >= min && value <= max;
}
}
// Usage
Range r = new Range(1, 10); // OK
Range bad = new Range(10, 1); // throws IllegalArgumentException
DDD Value Objects:
public record UserId(UUID value) {
public UserId {
Objects.requireNonNull(value, "UserId cannot be null");
}
public static UserId generate() {
return new UserId(UUID.randomUUID());
}
public static UserId of(String value) {
return new UserId(UUID.fromString(value));
}
}
public record Email(String value) {
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$");
public Email {
Objects.requireNonNull(value, "Email cannot be null");
value = value.toLowerCase().trim();
if (!EMAIL_PATTERN.matcher(value).matches()) {
throw new IllegalArgumentException("Invalid email format");
}
}
}
Best Practices
// ✅ Use compact constructor for validation
public record Age(int value) {
public Age {
if (value < 0 || value > 150) {
throw new IllegalArgumentException("Invalid age");
}
}
}
// ✅ For normalization of mutable objects
public record Tags(Set<String> tags) {
public Tags {
tags = Set.copyOf(tags); // immutable copy
}
}
// ❌ Don't use for complex logic
// ❌ Don't throw checked exceptions
// ❌ Don't access this before assignment
🎯 Interview Cheat Sheet
Must know:
- Compact constructor — form without signature, only for validation and normalization
- Compact constructor code is inlined into canonical BEFORE field assignment
- Parameters can be modified:
value = value.toLowerCase().trim()— this changes value before assignment - Cannot use
this.field = ...— only parameter name - Checked exceptions cannot be thrown directly (canonical constructor doesn’t declare throws)
- Cannot add fields in compact constructor
Common follow-up questions:
- Why is compact constructor needed? — For validation and normalization without boilerplate
- Can you change parameter value? — Yes, it’s normal practice:
value = value.trim() - Why can’t you use this.field? — Field assignment is generated automatically after compact constructor
- Can you throw checked exception? — No, must wrap in unchecked (IllegalArgumentException)
Red flags (DO NOT say):
- ❌ “Compact constructor assigns fields” — Assignment is automatic, only parameters can be changed
- ❌ “Compact constructor has a signature” — No signature, compiler determines by form
- ❌ “You can create an extra field” — Instance fields are forbidden in Record
- ❌ “You can throw IOException from compact constructor” — Only unchecked exceptions
Related topics:
- [[1. What is Record in Java and since which version are they available]]
- [[6. Can you override constructor in Record]]
- [[8. Can you declare static fields and methods in Record]]
- [[9. Are Record fields final]]