In my opinion, your rule is a good one (or at least it's not a bad one), but only because of the situation you are describing. I wouldn't say that I agree with it in all situations, so, from the standpoint of my inner pedant, I'd have to say your rule is technically too broad.
Typically you wouldn't define immutable objects unless they are essentially being used as data transfer objects (DTO), which means that they contain data properties but very little logic and no dependencies. If that is the case, as it seems it is here, I'd say you are safe to use the concrete types directly rather than interfaces.
I'm sure there will be some unit-testing purists who will disagree, but in my opinion, DTO classes can be safely excluded from unit-testing and dependency-injection requirements. There is no need to use a factory to create a DTO, since it has no dependencies. If everything creates the DTOs directly as needed, then there's really no way to inject a different type anyway, so there's no need for an interface. And since they contain no logic, there's nothing to unit-test. Even if they do contain some logic, as long as they have no dependencies, then it should be trivial to unit-test the logic, if necessary.
As such, I think that making a rule that all DTO classes shall not implement an interface, while potentially unnecessary, is not going to hurt your software design. Since you have this requirement that the data needs to be immutable, and you cannot enforce that via an interface, then I would say it's totally legitimate to establish that rule as a coding standard.
The larger issue, though, is the need to strictly enforce a clean DTO layer. As long as your interface-less immutable classes only exist in the DTO layer, and your DTO layer remains free of logic and dependencies, then you will be safe. If you start mixing your layers, though, and you have interface-less classes that double as business layer classes, then I think you will start to run into much trouble.
An http response can naturally be thought of as immutable, but only from the moment that it has been fully constructed and issued. Before that moment, various implementations of "response" classes in various environments are generally used as builders (see Builder Pattern) which get passed around in order for various routines to add to them piecemeal all the information that constitutes the final response. So, during the lifetime of the response as a builder, it is by its very nature mutable.
Now, it is still possible to take a class which is mutable by nature and turn it into an immutable class, but what I would like to suggest to you is that although doing so might be a fabulous exercise on paper, (the entire series of Erik Lippert's articles on immutability was nothing short of fascinating,) it is not really something that you would want to be doing in practice with any non-trivial class, because you end up with abominations precisely like the one you discovered. Plus, it is clearly not worth the effort, and it is stupendously wasteful in terms of computing resources.
So, my suggestion would be that if you really like the idea of an immutable response, (I like it too,) then rename your current response to ResponseBuilder
, let it be mutable, and when it is complete, create an immutable response out of it and discard the builder.
Best Answer
Then one thread makes one change (and gets one version of the object) and another thread makes another change (and gets a different, independent version of the object).
Nope, because each thread gets their own copy of the object.
They don't. That's the point.
And that's the gotcha. Immutability works great when things are values, or when things really are independent. But if you have an object that represents a single record - say from a database - then changes need to be merged back into that single record. That can be last in wins. That can be some manner of versioning/timestamping. There's lots of options with various tradeoffs.
But the key bit is that synchronization only needs to happen at that last step where the changes are merged and actually take effect on the single source of truth.