JEP 468 updating non-updatable fields
Andy Gegg
a.gegg at btinternet.com
Tue Jan 27 11:44:53 UTC 2026
Hello,
Thank you to all those who have spent time on this - I feel as if I
poked something that needed to be poked.
I know I talked about database ids which was a bad choice - as Brian has
said, they shouldn't really be there!
The point is that Records are lovely in so many ways that we're going to
use them where they're *almost* right - and withers will make that
easier and more appealing. But because records have no setters there's
no validation of the *change* except what the constructor can do - and
that's intended for use where its parameter values have to be taken as
correct (usually derived from a database, say), it cannot check a
*change* is legal as it has no knowledge of previous values. And so
business logic starts to be implemented in front end code (in the with
block), which is a Bad Thing.
Thank you again for your time,
Andy Gegg
On 25/01/2026 19:09, Brian Goetz wrote:
> 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/20260127/dde9374d/attachment-0001.htm>
More information about the amber-dev
mailing list