See what Java teams have to build manually to get trustworthy immutable models, and why that discipline still matters when moving to Clojure.
Java can absolutely be written in an immutable style.
Many strong Java teams already do some version of it:
final fieldsThat background helps when learning Clojure. The difference is not that Java cannot do immutability. The difference is that Java makes you enforce it deliberately, while Clojure gives it to you by default.
An immutable Java object normally follows a short checklist:
| Rule | Why it matters |
|---|---|
| Fields are private | Callers cannot mutate them directly |
| Fields are final | Construction is the main point where values are assigned |
| No setters | The public API does not expose mutation |
| Mutable inputs are copied | Callers cannot retain a handle and mutate shared state later |
| Mutable fields are returned defensively | Accessors do not leak writable internals |
That is a respectable engineering discipline. It is also a lot of discipline.
Here is a version of a Java value object that is genuinely trying to stay immutable:
1import java.math.BigDecimal;
2import java.util.List;
3
4public final class Invoice {
5 private final String id;
6 private final List<LineItem> items;
7 private final BigDecimal discount;
8
9 public Invoice(String id, List<LineItem> items, BigDecimal discount) {
10 this.id = id;
11 this.items = List.copyOf(items);
12 this.discount = discount;
13 }
14
15 public String id() {
16 return id;
17 }
18
19 public List<LineItem> items() {
20 return items;
21 }
22
23 public BigDecimal discount() {
24 return discount;
25 }
26
27 public Invoice withDiscount(BigDecimal newDiscount) {
28 return new Invoice(id, items, newDiscount);
29 }
30}
This is good Java.
It also shows the cost:
For one class, that is manageable. For a system full of nested models, the surface area adds up quickly.
Java developers also learn an important hard lesson: final is not enough by itself.
If a field points to a mutable object, the reference may be stable while the object still changes.
1public final class Schedule {
2 private final List<String> tasks;
3
4 public Schedule(List<String> tasks) {
5 this.tasks = tasks; // unsafe
6 }
7
8 public List<String> tasks() {
9 return tasks; // unsafe
10 }
11}
That object is not truly immutable, because callers can still mutate the shared List.
This is why Java immutability often turns into a policy problem:
Those questions are not academic. They are where many bugs come from.
Java records reduce a lot of boilerplate for data carriers, which is genuinely helpful.
But records do not magically deep-freeze their components. If a record field holds mutable state, the record is still only as immutable as its contents.
That means the same old questions still apply:
Records improve ergonomics. They do not change the core burden.
If you already value immutable Java design, you are not starting from zero.
You already understand:
What changes in Clojure is that you stop rebuilding this discipline class by class.
The language and standard collections start from value semantics instead.
Be careful not to overcorrect. Java immutability is still worth learning and using well because:
The better framing is:
That is a meaningful difference.