JEP 468 updating non-updatable fields
Brian Goetz
brian.goetz at oracle.com
Sun Jan 25 19:09:04 UTC 2026
The important mental model here is that a reconstruction (`with`)
expression is "just" a syntactic optimization for:
- destructure with the canonical deconstruction pattern
- mutate the components
- reconstruct with the primary constructor
So the root problem here is not the reconstruction expression; if you
can bork up your application state with a reconstruction expression, you
can bork it up without one.
Primary constructors can enforce invariants _on_ or _between_
components, such as:
record Rational(int num, int denom) {
Rational { if (denom == 0) throw ... }
}
or
record Range(int lo, int hi) {
Range { if (lo > hi) throw... }
}
What they can't do is express invariants between the record / carrier
state and "the rest of the system", because they are supposed to be
simple data carriers, not serialized references to some external system.
A class that models a database row in this way is complecting entity
state with an external entity id. By modeling in this way, you have
explicitly declared that
rec with { dbId++ }
*is explicitly OK* in your system; that the components of the record can
be freely combined in any way (modulo enforced cross-component
invariants). And there are systems in which this is fine! But you're
imagining (correctly) that this modeling technique will be used in
systems in which this is not fine.
The main challenge here is that developers will be so attracted to the
syntactic concision that they will willfully ignore the semantic
inconsistencies they are creating.
On 1/25/2026 1:37 PM, Andy Gegg wrote:
> Hello,
> I apologise for coming late to the party here - Records have been of
> limited use to me but Mr Goetz's email on carrier classes is something
> that would be very useful so I've been thinking about the consequences.
>
> Since carrier classes and records are for data, in a database
> application somewhere or other you're going to get database ids in
> records:
> record MyRec(int dbId, String name,...)
>
> While everything is immutable this is fine but JEP 468 opens up the
> possibility of mutation:
>
> MyRec rec = readDatabase(...);
> rec = rec with {name="...";};
> writeDatabase(rec);
>
> which is absolutely fine and what an application wants to do. But:
> MyRec rec = readDatabase(...);
> rec = rec with {dbId++;};
> writeDatabase(rec);
>
> is disastrous. There's no way the canonical constructor invoked from
> 'with' can detect stupidity nor can whatever the database access layer
> does.
>
> In the old days, the lack of a 'setter' would usually prevent stupid
> code - the above could be achieved, obviously, but the code is devious
> enough to make people stop and think (one hopes).
>
> Here there is nothing to say "do not update this!!!" except code
> comments, JavaDoc and naming conventions.
>
> It's not always obvious which fields may or may not be changed in the
> application.
>
> record MyRec(int dbId, int fatherId,...)
> probably doesn't want
> rec = rec with { fatherId = ... }
>
> but a HR application will need to be able to do:
>
> record MyRec(int dbId, int departmentId, ...);
> ...
> rec = rec with { departmentId = newDept; };
>
> Clearly, people can always write stupid code (guilty...) and the
> current state of play obviously allows the possibility (rec = new
> MyRec(rec.dbId++, ...);) which is enough to stop people using records
> here but carrier classes will be very tempting and that brings derived
> creation back to the fore.
>
> It's not just database ids which might need restricting from update,
> e.g. timestamps (which are better done in the database layer) and no
> doubt different applications will have their own business case
> restrictions.
>
> Thank you for your time,
> Andy Gegg
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20260125/455eca64/attachment-0001.htm>
More information about the amber-dev
mailing list