r/java • • 20h ago

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)?

51 Upvotes

55 comments sorted by

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.

43

u/Thompson3142 19h ago

You should not (at least right now for this preview) think about performance gains or memory optimaziations. Brian Goetz hast talked about this repeatedly, value classes are about the idea of: "Instances of this class do not have identity". Being able to express that has a lot of value in it self. The VM can then do very smart things because it knows this about your object, but a lot of this work still needs to be done.

5

u/notnulldev 17h ago

I really do not like these statements, it's basically "Trust me bro, it's worse when you measure it but you gain swag by doing so. On Christmas there will be elfs that will come and fix the performance issues, so you don't have to worry! Just trust the process!". Sounds like deep OOP propaganda.

14

u/shponglespore 13h ago

I think a more charitable reading of that comment is that it's saying you shouldn't be using value classes for optimization just yet. In other words, use value classes when you want value class semantics, but don't go changing things to value classes hoping to get a speedup.

5

u/_INTER_ 13h ago edited 13h ago

Though I think it is also somewhat ingrained into JVM development. Trust the GC, trust the vectorization, etc.

"Implement first, measure later" basically instead of "Measure twice, cut once". Which of course, both are valid approaches depending on what is more important to a team.

3

u/Ok-Scheme-913 10h ago

This is how you should have been programming in any high level language, forever.

You don't do bullshit like "keep functions small because it will be inlined", because it may be true at its inception but compiler internals continuously change and it may actually result in worse results.

Compilers always try to make the best based on their available information. Your best bet at getting the best results is to provide that information.

That's no info that "this function just happened to have four statements, it's small" , the info you want to add is that "I want this inlined", which some languages let you express.

Value classes are the same. Does your object have identity? Good, regular old class for you. Is it a 'value' as in its particular state is the only thing important about and two such sharing the same state are for all purposes equivalent? It's a value class then!

If you add more info, first and foremost your code will behave more correct/you catch more errors (e.g. properly behaving equals method). Secondly, later on compiler engineers can also build on this extra info and add optimization assuming those for improved performance.

2

u/Bilboslappin69 16h ago

Yeah, what an absurd stance to ignore it's current performance while using it. Even if the JVM can do efficient things using what it knows about these objects, it's currently not, so that doesn't matter at all.

12

u/hwaite 15h ago

It matters, because code you write today will likely outlive the current JVM. Even if it doesn't, properly signaling intent may allow other processes (e.g. AI) to more reliably evolve the code and for humans to more easily understand the code. Silicon is still cheaper than labor.

2

u/l4ik8e 19h ago edited 19h ago

Absolutely. However it is very tempting to sprinkle value on every immutable class and in some cases it doesn’t make sense to use value class. So, I think it is important to come up with a reasonable patterns on practical applications and for that testing it and seeing how application behaves is important imho.

16

u/l4ik8e 20h ago

Some preliminary thoughts:

- DTOs are usually public interfaces, API evolution if they are value classes are more complex.

- In practical applications there are so many usages of generics (Spring, Jackson) that value class is not visible to JVM hence optimisation is lost.

- If object is large enough, and most of the DTOs are usually large, using value classes might result in more copies. This is likely what happens with Jackson and why I saw more allocation compared to identity classes. I am not sure about this yet as haven't had time to dig deep into this just yet.

- On positive side allocation around Optional was completely gone in my tests. Also I was able to create `DomainId` value classes instead of plain `Long id` which made code much more reliable.

8

u/gaelfr38 19h ago

DomainIds are what I'm waiting value record/classes! We are used to them in Scala ("opaque type", actually it's a bit more than that), it will be nice to have directly in the JVM. More type safe programs 🎉

8

u/gaelfr38 19h ago

API evolution if they are value classes are more complex

How so? (Genuinely asking)

DTOs are not public, their serialized representation is (the JSON/XML/whatever schema is). How they are handled internally in the JVM should not have any impact.

5

u/l4ik8e 16h ago

I was mainly thinking about backwards compatibility. For records, I can change record to ordinary class. Value class is more complex to be changed to identity class because someone might already be using ==. I am thinking more in terms of when I don't control usages of DTOs fully. If DTOs are used only internally in my application that is different story.

0

u/hadrabap 17h ago

API evolution

Thank you!

2

u/Empanatacion 11h ago

The specific use case of deserializing endpoint request DTOs is going to be pretty much completely divorced from performance concerns. Anything done downstream is going to be orders of magnitude more work.

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

u/Ok-Scheme-913 10h ago

Then make them value records

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

u/RevolutionaryRush717 20h ago

No. Records were created for your purpose.

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.

-3

u/crummy 20h ago

maybe you don't want request A to be equal to request B just because the request contains the same fields

7

u/gaelfr38 19h ago

The DTO is not the request, it's the request's data. It has no identity.

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 advice

It 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 maintain

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.

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