Mutable and immutable classes are not just two object-modeling styles.
In concurrent systems, they create very different correctness and maintenance costs.
One pushes complexity into coordination. The other pushes complexity into construction and replacement.
That trade-off matters everywhere in backend design.
Problem Statement
Suppose you have a shared configuration object used by many request threads.
If the object is mutable, you must answer hard questions:
- who is allowed to update it
- how readers coordinate with writers
- whether several fields must change together
- how partially updated state is prevented
If the object is immutable, the problem changes:
- create a new snapshot
- publish it safely
- let readers keep using the old or new whole object
The second model is often dramatically easier to reason about.
Mutable Object Example
class MutableRoutingConfig {
private String primaryHost;
private String fallbackHost;
private int connectTimeoutMillis;
void setPrimaryHost(String primaryHost) {
this.primaryHost = primaryHost;
}
void setFallbackHost(String fallbackHost) {
this.fallbackHost = fallbackHost;
}
void setConnectTimeoutMillis(int connectTimeoutMillis) {
this.connectTimeoutMillis = connectTimeoutMillis;
}
}
This object may be totally fine in single-threaded or one-owner code.
In shared concurrent code, it creates several risks:
- one reader may see old
primaryHostwith new timeout - several setters may need one atomic business update
- safe publication of later mutations becomes your responsibility
The object is not wrong by itself. The sharing model makes it expensive.
Immutable Object Example
final class ImmutableRoutingConfig {
private final String primaryHost;
private final String fallbackHost;
private final int connectTimeoutMillis;
ImmutableRoutingConfig(String primaryHost,
String fallbackHost,
int connectTimeoutMillis) {
this.primaryHost = primaryHost;
this.fallbackHost = fallbackHost;
this.connectTimeoutMillis = connectTimeoutMillis;
}
String primaryHost() {
return primaryHost;
}
String fallbackHost() {
return fallbackHost;
}
int connectTimeoutMillis() {
return connectTimeoutMillis;
}
}
If this object is safely published, readers can use it without additional coordination because:
- fields do not change
- there is no mid-update state
- one object represents one stable snapshot
Runnable Example
public class MutableVsImmutableDemo {
public static void main(String[] args) {
MutablePricing mutablePricing = new MutablePricing(1000, "USD");
mutablePricing.setAmountCents(1200);
mutablePricing.setCurrency("EUR");
ImmutablePricing immutablePricing = new ImmutablePricing(1000, "USD");
ImmutablePricing updatedPricing = immutablePricing.withAmountCents(1200)
.withCurrency("EUR");
System.out.println("Mutable amount = " + mutablePricing.getAmountCents());
System.out.println("Mutable currency = " + mutablePricing.getCurrency());
System.out.println("Immutable old = " + immutablePricing.getAmountCents()
+ " " + immutablePricing.getCurrency());
System.out.println("Immutable new = " + updatedPricing.getAmountCents()
+ " " + updatedPricing.getCurrency());
}
static final class MutablePricing {
private long amountCents;
private String currency;
MutablePricing(long amountCents, String currency) {
this.amountCents = amountCents;
this.currency = currency;
}
long getAmountCents() {
return amountCents;
}
String getCurrency() {
return currency;
}
void setAmountCents(long amountCents) {
this.amountCents = amountCents;
}
void setCurrency(String currency) {
this.currency = currency;
}
}
static final class ImmutablePricing {
private final long amountCents;
private final String currency;
ImmutablePricing(long amountCents, String currency) {
this.amountCents = amountCents;
this.currency = currency;
}
long getAmountCents() {
return amountCents;
}
String getCurrency() {
return currency;
}
ImmutablePricing withAmountCents(long newAmountCents) {
return new ImmutablePricing(newAmountCents, currency);
}
ImmutablePricing withCurrency(String newCurrency) {
return new ImmutablePricing(amountCents, newCurrency);
}
}
}
This demo shows the mental difference:
- mutable objects change in place
- immutable objects produce a new value representing the next state
In concurrency, that difference is huge.
Why Immutable Often Wins in Concurrent Systems
Immutable classes make several hard problems disappear:
- no races on in-place mutation
- no need to lock for simple reads
- easier caching and reuse
- safer sharing across threads
- simpler reasoning about invariants
This is why immutable value objects work so well for:
- money
- configuration
- permission sets
- DTOs
- snapshot views
Why Mutable Still Exists
Mutable classes are still necessary and useful.
They are often the right model for:
- connection pools
- caches with internal eviction state
- schedulers
- queues
- resource managers
These objects are not pure values. They are stateful coordinators.
Trying to force everything into immutable snapshots can become awkward or wasteful.
Production-Style Comparison
Immutable is usually the better choice when:
- the object is widely shared
- reads dominate writes
- the object represents a business value or snapshot
- replacement is natural
Mutable is usually appropriate when:
- one component owns the state
- the object coordinates resources over time
- mutation is part of its identity and purpose
- shared access is already disciplined by confinement or locking
The key question is not “which is better.” The key question is “is this object a value to be shared or a stateful component to be controlled.”
Common Mistakes
- making an object mutable only because setters feel convenient
- making an object immutable on the surface while exposing mutable internals
- sharing mutable state broadly instead of narrowing ownership
- assuming immutable automatically means safe if publication is still broken
Immutability reduces concurrency burden a lot, but publication still matters.
Key Takeaways
- Immutable objects simplify concurrency because readers never race with in-place mutation.
- Mutable objects are still necessary for coordinators, pools, caches, and other stateful components.
- Default toward immutability for shared values and snapshots.
- Choose mutability deliberately, with a clear ownership and coordination model.
Comments