JEP 468 updating non-updatable fields
Brian Goetz
brian.goetz at oracle.com
Mon Jan 26 14:25:16 UTC 2026
> It's interesting that when language designers make the code easier to
> write, somebody may complain that it's too easy :-)
I too had that "you can't win" feeling :)
I would recast the question here as "Can Java developers handle carrier
classes". Records are restricted enough to keep developers _mostly_ out
of trouble, but the desire to believe that this is a syntactic and not
semantic feature is a strong one, and given that many developers
education about how the language works is limited to "what does IntelliJ
suggest to me", may not even _realize_ they are giving into the dark side.
I think it is worth working through the example here for "how would we
recommend handling the case of a "active" row like this.
> I think it's a perfect place for static analysis tooling. One may
> invent an annotation like `@NonUpdatable`
> with the `RECORD_COMPONENT` target and use it on such fields, then
> create an annotation processor
> (ErrorProne plugin, IntelliJ IDEA inspection, CodeQL rule, etc.), that
> will check the violations and fail the build if there are any.
> Adding such a special case to the language specification would be an
> overcomplication.
>
> With best regards,
> Tagir Valeev.
>
> On Sun, Jan 25, 2026 at 11:48 PM Brian Goetz <brian.goetz at oracle.com>
> 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/20260126/e9d075af/attachment-0001.htm>
More information about the amber-dev
mailing list