Yes, agreed. What I was hoping to illustrate was that the id (or a class fixing 
the id) has some precise semantics, relational semantics with and without 
provenance and that various APIs that already exist expose those in different 
ways that already leak those semantics. Dealing with those semantics directly 
isn't necessarily more dangerous than trying to obscure it: the sql in my 
example being more straightforward than the usual JPA while still safe.

Either you store provenance in an id class which only comes from the orm or in 
an unmodifiable field of a non-data class (which now can only come from the 
orm) or you let the api user manage provenance. I wouldn't want a record to 
represent provenace (so the id or the whole object in the second case) because 
I don't want to expose its constructor and I wouldn't use a carrier for the 
same reason. JEP 468 doesn't alter this story as far as I can see because if 
you want true provenance you need to control that behind a private/package 
protected constructor or some RAII: it isn't data alone, it is history. I also 
wouldn't really ever want a File or a Socket to be a record or carrier, but 2 
out of 3 of these database api approaches lend themself naturally and maybe 
safely to records and carriers: the first with provenance and the third 
without. JEP 468 doesn't change this because you already chose the approach 
when you made the constructirs.
On Monday, January 26th, 2026 at 4:55 PM, Brian Goetz <[email protected]> 
wrote:

> I think the entity-modeling story here is more like:
>
> - a regular class that associates (id, PersonInfo), where the ids are 
> dispensed exclusively by the ORM, and
> - a record/carrier for PersonInfo, that lets you "mutate" the information but 
> not the ID association.
>
> On 1/26/2026 10:52 AM, Aaryn Tonita wrote:
>
>> This past sprint we had such a case where unconditional deconstruction would 
>> have helped with database entities. Basically a user had created a patient 
>> twice over quite some time span and operations and graft allocations were 
>> associated with both patients but the medical and personal details were most 
>> accurate on the newest and the user desire was to merge them. Our 
>> deduplication detection didn't trigger because of incompleteness of the old 
>> record. However because so many downstream systems depend on the oldest 
>> record that was the id to keep.
>>
>> In the database you would just alter the id of the old with cascade, then 
>> relink the foreign key constraints of related tables to the new patient, 
>> then modify the id back to the old value again with cascade and delete the 
>> old. Now JPA doesn't support this approach of altering a primary key... So 
>> instead you need to fetch the new and old entity and deconstruct the new 
>> entity before deleting it and then reconstruct the old entity with the new 
>> data and same old id... Or you reach for the @Query approach instead and 
>> after modifying the database you change just the id (and linked relations) 
>> of the newer patient representation object. This latter approach is less 
>> brittle to future changes.
>>
>> But the original point that Brian made stands: the constructor always allows 
>> a nonsense representation. People exploit that in unit tests to create 
>> unpersisted entities or relations to other entities that don't exist. 
>> Without fetching the entire database all at once you won't really get away 
>> from that but I also wouldn't want to.
>>
>> We have more and more places where withers would help (and sad places where 
>> a carrier class would have helped but we used a class in place of a record).
>>
>> Sent from [Proton 
>> Mail](https://urldefense.com/v3/__https://proton.me/mail/home__;!!ACWV5N9M2RV99hQ!M9c8nZS-3Ga3cPLqwU5QkNrUJs_RoN8etK5ZrHbzmmz29wzkUXoSJmTLdIVhe9GYuWhACJi2C0eIRWF10YI$)
>>  for Android.
>>
>> -------- Original Message --------
>> On Monday, 01/26/26 at 16:13 Ethan McCue 
>> [<[email protected]>](mailto:[email protected]) 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 <[email protected]> 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 <[email protected]> 
>>>>> 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

Reply via email to