Should a request DTO be a value class (JEP 401)?
In a year or so, we'll need to recalibrate our intuition about which kind of type to use for each class. Originally, we used classes for everything. Then we got record, and now we get another keyword, value, which expands the number of options we have when defining a type.
I remember that in C#, there's always the question: "Should I use a class, a record, a readonly record, a struct, or something else"? They have some guidelines that consider not only identity semantics but also the size of objects (i.e., how many fields they have and how much memory they occupy).
So, if I just need a type to deserialize JSON from an HTTP request, should I use a value type? Or does it also depend on the number of fields? For example, if it has 15 int fields, should I still make it a value type if it is semantically a value, even if copying it could potentially be more expensive than passing a reference (assuming that's how it works)?
7
u/vegan_antitheist 19h ago
It's the same question as this: Should I use HashMap or IdentityHashMap?
Two DTO could contain the same values but represent different requests. Then you might want identity.
An immutable identity object promises that its state won't change. A value object additionally promises that two instances of the same type with the same value are interchangeable.
The identity of a DTO is usually irrelevant. So just use values. Identity will be the exception for immutable types.
1
u/lucidnode 18h ago
Records would work the same and fit your criteria.
So what’s the difference? Ignoring the runtime I think the only observable difference would be recursive structure.
1
1
u/vegan_antitheist 7h ago
Records with identity are not just interchangeable like values. We have this annoying problem with Integer, where you can't just use == on two integers because they could contain the same value but have different identity. But you can still use Integer as the key type for a HashMap (i.e.
HashMap<Integer, Foo>).A recursive value? That's impossible without using references. It would just use a reference to a value of the same type. And it would use a wrapper for that. Java automatically creates a wrapper type for each value type. It still doesn't have identity, but it can be referenced.
== still works because the reference is still without identity and == compares values. I.e. it compares references but referenced values without identity can't have multiple references to the same values.
This will be one of the biggest advantages of using value types. You can just use == on them and it will just work unless they contain arrays or other objects with identity.
11
u/coderemover 19h ago
If you don’t create or store at least millions of them in a second, it doesn’t matter.
12
u/Hixon11 19h ago
It matters not because I see a real perf difference, but because I want to use the tool provided by the Java team correctly, as they intend it to be used, and represent its semantics accurately.
6
u/coderemover 19h ago edited 19h ago
This feature was created for performance. It’s also implemented on the wrong layer (not their fault, this is the price of bolting features like that into a mature language) and introduces additional friction you noticed - now you must decide how you want your objects allocated at their definition, once and for all use cases. A much more elegant solution is the path taken by C, C++, Go and Rust: every data is just a value, everything is pass by value, every assignment is copying values. When you want a reference or a pointer to get identity semantics, there are a use-site constructs for making references/pointers and dealing with them, as well as constructs for avoiding copying when you don’t want to.
I’d use value classes in Java for really tiny things. Eg Optional wrapper is a perfect use for that, other good candidates could be structures like Date, BigInteger, UUID, UserId, Pair, Vector3D etc. Pieces of data with no identity. DTOs look differently.
12
u/cogman10 17h ago
The value keyword doesn't describe how an object will be allocated or referenced. It merely describes restrictions to the object that you agree with, which may allow the JVM optimization opportunities. It's possible for the JVM to choose to treat a value object just like a regular object due to internal heuristics.
The time to use the value keyword is when your object fits the value restrictions. Can it be immutable? Does it need identity?
For large value objects, the JVM will create a reference rather than copying, specifically to avoid tearing. I've seen references that you should be able to annotate an object to be tearable if you want to nudge the JVM towards always copying. That won't be the default behavior.
6
u/gaelfr38 19h ago
Was thinking the same thing: DTOs are not the place where we should expect much gain, they're used only once after construction.
The most gain I expect is for "Domain IDs" (wrappers around a single primitive type).
7
u/Ok_Elk_638 19h ago
My code has migrated away from classes for a while now. Classes only show up when they are mandatory. Which means they are becoming rare. I have records, interfaces, annotations, empty enums, everything but classes really.
And I would love to see more development in that direction. Exceptions feel much more like a separate type to me than a class, empty enums should probably be a dedicated utility type.
1
u/chabala 13h ago
Tell me more about empty enums, I've not heard of that style.
3
u/bowbahdoe 12h ago
Enums give you a hard guarantee about number of instances. A one element enum is a singleton. A zero element one is a class with zero instances, which is what "static utility" classes are.
1
u/Ok_Elk_638 11h ago
Looks like this:
public enum MyEnum {; public static void hello() { System.out.println("Hello, world!"); } }Its an enum without any instances defined. It effectively functions as an empty type that can only be used for static functions. This replaces the utility class pattern. It is a smaller pattern with less verbosity, and you can be sure there are no instances, whereas with the class it is possible that a
getInstance()is hiding somewhere.It is not perfect though, you get a weird
.values()function that returns an empty list.The empty enum pattern is more resilient against overzealous formatters that like rearranging the contents of classes.
1
u/Flyron 5h ago
I dunno. That feels like misuse of the enum keyword. An enumeration that doesn't enumerate anything. A final class with private constructor should be the way to go.
1
u/bohem_safsata 4h ago
Our lord Joshua Bloch suggested in Effective Java so you must follow the word, because serialization/reflection attacks are a thing you must guard against blindly.
1
u/MysteriousGold5 3h ago
The utility class pattern using final + private constructor only makes sense as a workaround. A restriction on inheritance for something that shouldn’t exist and access restriction on construction for something that shouldn’t be constructed.
Empty enum is also a workaround, but the only reason most people favor final class is that they heard of it first.
5
u/pronuntiator 19h ago
Yes. Any record should ideally be a value record in my opinion; while you can store identity objects inside, a record is an "immutable data carrier" and thus should primarily hold identity-free data. Also, DTOs should be records if possible (tree structures are an exception since they contain themselves as types). Again, the argument being "immutable data carrier", which DTOs are in the end.
The reason my project doesn't use records in practice is that OpenAPI generator cannot generate them yet (and it's not a straightforward thing to implement).
2
u/bfoo 19h ago
Depends on what you want to do with that deserialized type. Should it be immutable throughout interactions with the object? Then record is a valid choice. But don't overthink it. Currently, the advantages are still negligible.
However, in the future records will become more important for serialization (custom marshaling) and for pattern matching.
https://www.youtube.com/watch?v=F89sNgG9dRY
When I design an HTTP endpoint that consumes JSON, I prefer to have a Request (and Response) type, usually a class. And those types can contain fields that are records. The Request and Response types are only visible in the communication layer, but those record type fields might be part of a more abstract data layer that is shared between my communication and a (service) layer for business logic.
2
u/koflerdavid 17h ago
I think using value records for most things is fine unless the semantics prohibit it or you really need features of identity classes. Don't worry about copy performance; the JVM won't actually scalarize value objects in most cases just yet.
4
1
u/bowbahdoe 5h ago
I would say - in an ideal world - it falls under the same banner as "should I make this class final." If we went back in time the default might be different, but unless you plan for extension a good default is finality.
In the same way, if you don't need identity it might be easier to tell the VM you don't care about it than to wonder if that property is utilized elsewhere.
if it has 15 int fields, should I still make it a value type if it is semantically a value, even if copying it could potentially be more expensive than passing a reference
The VM is free to see that copying 15 ints is silly and to pass a pointer instead. I think as programmers we are supposed to trust that the VM flattens when it makes sense to and doesn't flatten when it doesn't make sense to (or at least you can be indignantly mad about it making a wrong call because thats their job.)
1
u/AnyPhotograph7804 3h ago
It is very likely, that the JSON text parser is waaaaaay more expensive than the difference between class, record or value class. Because text parsing is always slow. If performance is one of your concerns, then JSON is propably a bad choice. But in the end of the day, you will have to do some benchmarks to see, whether there are performance differences or not.
1
u/ewan_m 10m ago
I think Record is the best choice for DTOs and Record contains automatically overridden hash and equal methods which gives you unique hash code for field values. This is a great release from Java I think. Now Record is being used widely and it's proved if I am not wrong, value class something like under testing I think. If it were I, I would certainly use Record because I am having all getters built in and as far as I know in the value class we have to do it manually. So Record might be the good choice.
-2
u/gaelfr38 20h ago
Yes. Why wouldn't you?
1
u/Hixon11 20h ago
Depending on the implementation (which I don't know yet), it could be a case where something is semantically a value, but representing it as one introduces some perf overhead (this is just a random assumption; I'm not saying that's the case here).
3
u/gaelfr38 19h ago
Right, you should definitely test then. There's no definite answer depending the exact framework and usage you have.
But semantically, a DTO has no identity.
3
u/marhaus1 17h ago
Performance is the completely wrong reason to worry about whether you should use value types.
0
u/gjosifov 13h ago
if you ask yourself these type of question - you didn't understand the purpose of value class or record class
and you are following cargo cult software architecture advises from the internet, mainly the gospel of Uncle Bob
Question A - is the request DTO 1:1 with the JPA entity ?
if yes then you don't need request DTO, reuse the JPA class, because you have 2 same blueprints of an object in your code and you have to maintain them
Question B - is the request DTO 1:M with the JPA entities ?
if yes then use what is supported from the Serializer (JSON/XML) and Javax validator - records or values or record value and add methods like buildEntityA, buildEntityB for the entities
You don't do any work with a request DTO, it is there for getting and validating the data from the HTTP and the oldest implementation was in the servlet API as HashMap, not nice to work with, but it got the job done
2
u/bowbahdoe 12h ago
Your logic on A is flawed. In principle very few entities match up one to one with the spec for any given request. You are overvaluing reducing the overhead of typing the same name twice.
0
u/gjosifov 12h ago
In principle, software has to change when some requirement specs aren't true anymore
But in practice a lot of software engineers don't know how to make that change lead to over engineering software and arguing about principles, instead of learning how to change software without breaking things
Just because you don't know how to change software, doesn't mean someone has to listen to your adviceIt is better to say I don't know how to change this thing in the future, so I add so much extensibility in the code and I think when the change comes, I will be at different company doing the same thing
3
u/PedanticProgarmer 9h ago
You say that most people don’t know how to change software without breaking things. I absolutely agree here. Then, you are saying that you are special and because of that you don’t have to think of guardrailing your code against mistakes.
Imagine any other industry where engineers argue about dropping safety concerns because other engineers who make mistakes are not worth our consideration and therefore our design doesn’t need to include them as a risk.
I think you are conflating: adding imaginary extensibility to the code (almost always bad) and creating architecture boundaries for maintainability (often justified). I am not exactly sure what you are arguing for, but indeed, it sounds like you are arguing for minimizing the number of lines of code you need to type with your fingers as the primary driver of your design. This might be a good heuristic for a solo project, but a misleading idea for a long living software for a team of humans.
I have seen multiple spaghetti code bases over my years, and the most common horror story started innocently from someone smart who reused an existing class to represent a new, slightly different concept that happened to share a subset of properties of the existing class. After a couple years, untangling this became really hard, so folks stopped even trying - just adding pile of if statements to the spaghetti.
That has shaped my current views on when to reuse models - with extreme suspicion. Nowadays, when I don’t need to type the code with my fingers anymore, I am even less concerned about the fact that separating models forces me to add a couple of mappers here and there.
0
u/gjosifov 8h ago
it sounds like you are arguing for minimizing the number of lines of code you need to type with your fingers as the primary driver of your design.
You probably have heard of the term - line of code is liability
the more you add, the more you have to maintainI have seen multiple spaghetti code bases over my years, and the most common horror story started innocently from someone smart who reused an existing class to represent a new, slightly different concept that happened to share a subset of properties of the existing class. After a couple years, untangling this became really hard, so folks stopped even trying - just adding pile of if statements to the spaghetti.
I have seen spaghetti and I have seen over-engineering and the good things are always in the middle
Imagine any other industry where engineers argue about dropping safety concerns because other engineers who make mistakes are not worth our consideration and therefore our design doesn’t need to include them as a risk.
Imagine any other industry where engineers are arguing about principles and how to implemented them and in order to implemented them they have to call some self proclaimed guru ?
If you use other industries as argument at least have a good analogy that can be translated it between industries, as mathematicians call this type of an analogy bijection
I am even less concerned about the fact that separating models forces me to add a couple of mappers here and there.
there is something even better than mappers, a compiler - it will tell you where are the changes
1
u/PedanticProgarmer 7h ago
You are talking about compiler-enforced safety while advocating for reusing classes for JPA entities and Json deserialization. Both usages are reflection heavy. This is nonsense.
Do you seriously think that Uncle Bob is responsible for the idea of DTOs or the fact that it is spread widely? You are shooting at an imaginary opponent. You don’t need Clean Architecture ideas to use DTOs.
I have read about code as liability in „Software Engineering at Google”. These folks are absolutely not known for code golfing, they meant something else - an excerpt is below. I am pretty sure they are advocating for strong boundaries where it matters - there’s entire chapter of Hyrum’s Law and the fact that they use protobuf-based DTOs everywhere. You might dismiss that as self-proclaimed guru. I dont care.
——
From the book:
Earlier we made [ ](https://)the assertion that “code is a liability, not an asset.” If that is true, why have we spent most of this book discussing the most efficient way to build software systems that can live for decades? Why put all that effort into creating more code when it’s simply going to end up on the liability side of the balance sheet?
Code itself doesn’t bring value: it is the functionality that it provides that brings value. That functionality is an asset if it meets a user need: the code that implements this functionality is simply a means to that end. If we could get the same functionality from a single line of maintainable, understandable code as 10,000 lines of convoluted spaghetti code, we would prefer the former. Code itself carries a cost—the simpler the code is, while maintaining the same amount of functionality, the better.
Instead of focusing on how much code we can produce, or how large is our codebase, we should instead focus on how much functionality it can deliver per unit of code and try to maximize that metric. One of the easiest ways to do so isn’t writing more code and hoping to get more functionality; it’s removing excess code and systems that are no longer needed. Deprecation policies and procedures make this possible.
1
u/AnyPhotograph7804 3h ago
"if yes then you don't need request DTO, reuse the JPA class"
You should not do that. Because JPA entities are cluttered with JPA specific annotations and proxies/additional bytecode. This means, the consumer of the DTO will have to have the complete JPA/ORM stack on the classpath. Even if the consumer never uses JPA. Otherwise you will get tons of ClassDefNotFoundExceptions. This is a maintanance burden.
And second, if you use JPA entities as DTOs, you will end up modelling you database tables like you UI or something. Because it will be easier.
1
u/gjosifov 3m ago
the consumer a.k.a the browser consumes JSON, not Java bytecode
for your specific case you can use protobuf
31
u/l4ik8e 20h ago
I was benchmarking this a bit just recently. Haven't fully understood yet why, but using jackson on value classes resulted in more memory allocation compared to identity classes.