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