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