When should you choose a record over a regular class, and what are the trade-offs?
answer
- Record = immutable, value-identity data carrier
- Great for DTOs, keys, tuples, sealed-event hierarchies
- No mutability, no class inheritance, no hidden instance state
- Accessors are x() not getX()
- Defensively copy mutable components
basics
~20 sUse a record when you need an immutable carrier of data whose identity is its values - like a DTO, a key, or a return tuple. Avoid records when you need mutability, inheritance, hidden/derived state, or JavaBean-style getters that frameworks expect.
solid answer
~50 sRecords shine as transparent, immutable value holders: their identity is their components, they give you free value-based equals/hashCode/toString, and the constructor can enforce invariants. Reach for one for DTOs, compound map keys, multi-value return types, and message/event objects. The trade-offs come from the constraints. Records are final and can't extend a class, so no implementation inheritance - share behavior via interfaces instead. They're immutable, so they're a poor fit for entities you mutate over time (e.g. mutable JPA entities). Accessors are `x()` not `getX()`, so tools expecting the JavaBean convention may need configuration. All state must be in the components, so you can't hide computed/cached fields as instance state (you compute in methods or via statics). And components that are mutable types aren't deeply immutable unless you defensively copy. If you need none of those, a record removes a lot of boilerplate safely.
go deeper
Picks a record for simple immutable data and knows it's not for things that change.
Lists concrete fits (DTO, key, tuple) and concrete misfits (mutable entity, inheritance, bean getters).
Weighs deep-immutability of mutable components, framework getter expectations, and pairs records with sealed interfaces for domain modeling.
Sets codebase-wide guidance on records vs entities vs Lombok, migration cost, serialization/compatibility, and the value-object boundary in the domain model.
### What a record gives you A record auto-generates a canonical constructor, per-component accessors, and value-based `equals`/`hashCode`/`toString`. Its defining idea is that **identity = the component values**: two records are equal exactly when their components are equal. It's immutable (component fields are `final`), `final` (can't be subclassed), and extends `java.lang.Record` (can't extend another class) but can implement interfaces. ### When a record is the right tool - **DTOs / API payloads** - immutable, self-describing data crossing a boundary. - **Map keys / set elements** - free, correct value-based `equals`/`hashCode`. - **Multi-value returns / tuples** - return two or three named values without an out-param or a Pair library; often as a *local record*. - **Events / messages** - immutable facts; pairs naturally with **sealed interfaces** and pattern matching for exhaustive `switch`. - **Value objects (DDD)** - things defined by their attributes, not identity. ### When NOT to use a record - **You need mutability.** Records are immutable; a stateful, long-lived, mutable object (e.g. a builder, a mutable domain entity) doesn't fit. *Example:* a JPA `@Entity` is typically mutable and needs a no-arg constructor and proxying - records clash with that. - **You need implementation inheritance.** Records can't extend a class. If you want to inherit fields/behavior from a base class, use a normal class. (You can still implement interfaces.) - **You need hidden or cached instance state.** All state must be in the components. You can compute derived values in methods or cache via a *static* structure, but you can't add a non-static cache field. - **Frameworks expect JavaBean getters.** Accessors are `x()`, not `getX()`. Older reflection-based tools (some serializers, templating) may need configuration or won't bind. (Modern Jackson etc. support records.) - **Mutable components.** A record holding a `List` or array isn't deeply immutable unless you defensively copy in the constructor and accessor; otherwise callers can mutate its internals. ### Records vs Lombok / hand-written POJOs Records are a *language* feature, so no annotation processor, no generated-source surprises, and guaranteed consistent `equals`/`hashCode`. Lombok `@Value` is the closest analog but is third-party and works on mutable-capable classes; a record is the standard, zero-dependency choice for immutable data. ### The mental checklist Ask: *Is this object fully described by its data, immutable, and not part of an inheritance hierarchy?* If yes - record. If it needs to change over time, inherit implementation, hide state, or expose bean getters a framework requires - regular class. ### Why it matters Choosing a record communicates intent ('this is immutable data') and removes a large class of boilerplate bugs (mismatched `equals`/`hashCode`, forgotten fields in `toString`). Choosing a class where a record doesn't fit avoids fighting the constraints. The senior skill is recognizing which side of the line a given type falls on.
- Why are records a strong fit for sealed-interface event hierarchies?Each event is an immutable fact best modeled as a value, and records give you that with free equals/toString. Combined with a sealed interface, the compiler can verify a switch covers every case (exhaustive pattern matching), giving safe, concise domain modeling.
- A record has a List component. Is it immutable?Not automatically. The reference is final, but callers can mutate the list unless you copy it in the constructor (List.copyOf) and return a copy/unmodifiable view from the accessor. Only then is it effectively immutable.
saying these in an interview costs you the question
- Using a record for a mutable JPA entity
- Expecting getX()-style accessors
- Assuming a record with a List component is deeply immutable
- Trying to share code via a base class instead of an interface
- Treating records as just 'less typing' without the immutability/value-identity intent