JEP 468 updating non-updatable fields

Brian Goetz brian.goetz at oracle.com
Mon Jan 26 15:25:03 UTC 2026


I don't think adding the conditional deconstruction story (as 
interesting as it is!) would shed light on this question.  I think the 
answer lies in "why does the active-row pattern not obey the 
requirements for being a carrier."  Whether deconstruction is 
conditional or unconditional, the problem is still that if someone can 
create records/carriers with

     PersonRow r = new PersonRow(rand.nextInt(), "Bob Smith")

and then persist them with

     database.persist(r);

they are bestowing the right to update random rows that were not 
dispensed by the ORM.  The database API has, by virtue of the fact that 
PersonRow has a public constructor that accepts an ID, essentially 
exposed a wider API than it intended to.

The answer has always been "don't use carriers/records for this", but 
the interesting sub-question is (a) how to explain this succinctly to 
users so they get it and (b) what to tell them to do instead.


On 1/26/2026 10:12 AM, Ethan McCue wrote:
> My immediate thought (aside from imagining Brian trapped in an eternal 
> version of that huffalumps and woozles scene from Winnie the Pooh, but 
> it's all these emails) is that database entities aren't actually good 
> candidates for "unconditional deconstruction"
>
> I think this because the act of getting the data from the 
> db/persistence context is intrinsically fallible *and* attached to 
> instance behavior; maybe we need to look forward to what the 
> conditional deconstruction story would be?
>
> On Mon, Jan 26, 2026, 10:04 AM Brian Goetz <brian.goetz at oracle.com> wrote:
>
>
>
>>     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/b452ee8d/attachment-0001.htm>


More information about the amber-dev mailing list