I've done it in Kotlin, and I suspect that modern Java should be quite amenable to it with records.
It was really nice in my Kotlin project because we were dealing with legacy data structures with very confusing names—being able to guarantee that a UserID doesn't accidentally get passed where a UserDataID was expected helped prevent a lot of the bugs that plagued the legacy apps.
That's great to hear. We did the same, but in C#, using its records. The codebase didn't exactly suffer from errors from ID misuse (all of which were the same type beforehand), but it's great for future-proofing as well.
Added benefit, as always when leaning into the type system more, is a reduction in the number of unit tests required. The need to test that `update_user(group_id)` fails (because a non-user ID was passed) simply disappears.
In Scala it worked great using AnyVal wrappers around primitive types. It’s something I miss in typescript, where type aliases are more for documentation purposes on id types but don’t add much type safety. I think they trick is the type needs value semantics, which records should help with.
It was really nice in my Kotlin project because we were dealing with legacy data structures with very confusing names—being able to guarantee that a UserID doesn't accidentally get passed where a UserDataID was expected helped prevent a lot of the bugs that plagued the legacy apps.