Definition
- Prototype pattern – creates new objects by copying an existing instance (the prototype) rather than constructing from scratch.
- Shallow copy – duplicates the top-level object but copies only the references of its fields, not the objects they point to.
- Deep copy – recursively copies inner mutable objects so the copy is fully independent.
- Cloneable – a marker interface with no
clonemethod; usesObject.clone()which is shallow by default and bypasses constructors. - Copy constructor – a constructor that takes an instance of the same class and copies its state explicitly; recommended by Joshua Bloch.
Imagine National Bank of India has a standard loan-application template pre-filled with the bank's default terms. When Rajesh applies, the clerk photocopies the template and just changes the applicant details. But if the template has a stapled booklet of sub-documents and the photocopy only copies the cover page while still pointing to the same shared booklet, then editing Rajesh's booklet accidentally changes Priya's too. That shared-booklet problem is the shallow-clone trap.
Understanding the Concepts
What is the Prototype pattern?
Prototype answers "I need another object just like this one." Rather than calling a complicated constructor and reconfiguring everything, you copy a prototype that is already set up.
package com.bank.model;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class Account {
private String accountId;
private BigDecimal balance;
private List<String> transactionIds;
public Account(String accountId, BigDecimal balance, List<String> transactionIds) {
this.accountId = accountId;
this.balance = balance;
this.transactionIds = new ArrayList<>(transactionIds);
}
// Copy constructor – deep copy
public Account(Account other) {
this.accountId = other.accountId;
this.balance = other.balance;
this.transactionIds = new ArrayList<>(other.transactionIds);
}
public void addTransaction(String txnId) {
transactionIds.add(txnId);
}
public List<String> getTransactionIds() {
return List.copyOf(transactionIds);
}
@Override
public String toString() {
return accountId + " bal=" + balance + " txns=" + transactionIds;
}
}
What is the shallow-clone trap?
A shallow copy duplicates the top-level object but copies only the references. If an Account holds a List, a shallow copy gives you a new Account whose list field points to the very same list.
package com.bank.demo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class ShallowCloneTrap {
public static void main(String[] args) {
List<String> txns = new ArrayList<>(List.of("TXN-001"));
// Simulate shallow copy (just copy references)
Account original = new Account("ACC-001", new BigDecimal("100000"), txns);
// SHALLOW: shares the same list!
Account shallowCopy = new Account("ACC-001", original.balance, original.transactionIds);
// If transactionIds were the SAME reference:
// shallowCopy.addTransaction("TXN-HACK");
// original would also show TXN-HACK!
// DEEP COPY: uses copy constructor
Account deepCopy = new Account(original);
deepCopy.addTransaction("TXN-002");
System.out.println("Original: " + original);
System.out.println("Deep copy: " + deepCopy);
}
}
Original: ACC-001 bal=100000 txns=[TXN-001]
Deep copy: ACC-001 bal=100000 txns=[TXN-001, TXN-002]
The deep copy has its own independent list. The original is untouched.
Why is Cloneable problematic?
Joshua Bloch, in Effective Java, criticises Cloneable harshly:
Cloneableis a marker interface with noclonemethod – it merely changes the behaviour ofObject.clone(), which is confusing.clone()is declared onObjectas protected and returnsObject, so callers need casts.clone()does not run constructors, so it can produce an object in an invalid state.clone()cannot assignfinalfields, which conflicts with immutability.- It throws the checked
CloneNotSupportedExceptioneven when cloning is valid. - The contract between
clone,super.clone, and deep copying is fragile.
package com.bank.model;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
// PROBLEMATIC: using Cloneable
public class AccountCloneable implements Cloneable {
private String accountId;
private BigDecimal balance;
private List<String> transactionIds;
public AccountCloneable(String id, BigDecimal bal, List<String> txns) {
this.accountId = id;
this.balance = bal;
this.transactionIds = new ArrayList<>(txns);
}
@Override
public AccountCloneable clone() {
try {
AccountCloneable copy = (AccountCloneable) super.clone();
// MUST manually deep-copy mutable fields!
copy.transactionIds = new ArrayList<>(this.transactionIds);
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError("Should not happen", e);
}
}
}
Why is a copy constructor preferred?
A copy constructor is explicit, type-safe, respects final fields, and you control exactly what is copied.
package com.bank.model;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class AccountSafe {
private final String accountId;
private final BigDecimal balance;
private final List<String> transactionIds;
public AccountSafe(String id, BigDecimal balance, List<String> txns) {
this.accountId = id;
this.balance = balance;
this.transactionIds = List.copyOf(txns);
}
// Copy constructor: explicit, works with final fields
public AccountSafe(AccountSafe other) {
this.accountId = other.accountId;
this.balance = other.balance;
this.transactionIds = List.copyOf(other.transactionIds);
}
// Static copy factory (alternative form)
public static AccountSafe copyOf(AccountSafe other) {
return new AccountSafe(other);
}
@Override
public String toString() {
return accountId + " bal=" + balance + " txns=" + transactionIds;
}
}
- Benefits over
Cloneable: - Runs a real constructor, so the object is always valid.
- Works with
finalfields. - No casts, no checked exceptions.
- Deep-copy logic is explicit and visible.
When is clone acceptable?
For arrays, clone() is fine and idiomatic because arrays have a well-defined, efficient clone:
int[] original = {1, 2, 3, 4, 5};
int[] copy = original.clone(); // perfectly fine for arrays
For genuinely immutable classes, copying is unnecessary: you can safely share the same instance since nothing can mutate it.
What are the use cases for Prototype?
- Expensive construction: building an object requires a database call or heavy computation; cloning avoids repeating that cost.
- Pre-configured templates: keep a prototype with defaults and clone it for each use.
- Runtime type selection: copy an object of unknown concrete type without knowing the type statically.
- Snapshotting: clone captures the current state for what-if scenarios.
package com.bank.demo;
import java.math.BigDecimal;
import java.util.List;
public class PrototypeDemo {
public static void main(String[] args) {
// Template: pre-configured order
AccountSafe template = new AccountSafe("TEMPLATE", new BigDecimal("0"), List.of("SETUP"));
// Prototype: copy and customise
AccountSafe rajesh = new AccountSafe(template);
AccountSafe priya = AccountSafe.copyOf(template);
System.out.println("Template: " + template);
System.out.println("Rajesh: " + rajesh);
System.out.println("Priya: " + priya);
}
}
Template: TEMPLATE bal=0 txns=[SETUP]
Rajesh: TEMPLATE bal=0 txns=[SETUP]
Priya: TEMPLATE bal=0 txns=[SETUP]
How do you test that deep copy is correct?
A proper deep-copy test verifies that modifying the copy does not affect the original. This is the definitive test for the shallow-clone trap: mutate every mutable field in the copy and assert the original is unchanged.
package com.bank.model;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
public class DeepCopyTest {
static class AccountTemplate {
String type;
BigDecimal balance;
List<String> features;
AccountTemplate(String type, BigDecimal balance, List<String> features) {
this.type = type;
this.balance = balance;
this.features = features;
}
// Copy constructor (deep copy)
AccountTemplate(AccountTemplate source) {
this.type = source.type;
this.balance = source.balance;
this.features = new ArrayList<>(source.features);
}
}
public static void main(String[] args) {
AccountTemplate original = new AccountTemplate(
"SAVINGS", new BigDecimal("50000"), new ArrayList<>(List.of("ATM", "UPI")));
AccountTemplate copy = new AccountTemplate(original);
// Mutate the copy
copy.features.add("CHEQUE");
copy.type = "CURRENT";
// Original must be unaffected
System.out.println("Original features: " + original.features);
System.out.println("Copy features: " + copy.features);
System.out.println("Deep copy correct: " + !original.features.contains("CHEQUE"));
}
}
Expected output:
Original features: [ATM, UPI]
Copy features: [ATM, UPI, CHEQUE]
Deep copy correct: true
What should a senior engineer know?
- The most common mistake is a shallow copy that accidentally shares mutable state.
- In modern Java, records with a compact copy helper reduce the need for manual clone logic.
- Bloch's criticism of
Cloneableis a standard interview topic; know the five problems. - For banking what-if scenarios, deep-copy an account to simulate transactions without touching the real one.
Visual Overview
Shallow vs Deep Copy
─────────────────────
SHALLOW COPY:
┌──────────────┐ ┌──────────────┐
│ Original │ │ Shallow Copy │
│ accountId │ │ accountId │
│ balance │ │ balance │
│ txnIds ──────┼──┐ │ txnIds ──────┼──┐
└──────────────┘ │ └──────────────┘ │
│ │
└──── SAME LIST ─────┘
Mutating one mutates both!
DEEP COPY:
┌──────────────┐ ┌──────────────┐
│ Original │ │ Deep Copy │
│ accountId │ │ accountId │
│ balance │ │ balance │
│ txnIds ──────┼──┐ │ txnIds ──────┼──┐
└──────────────┘ │ └──────────────┘ │
▼ ▼
[TXN-001] [TXN-001] (independent)
Key Takeaway
The Prototype pattern creates a new object by copying an existing, pre-configured instance rather than building from scratch. Rajesh uses it at National Bank of India for what-if scenarios: he deep-copies a real account to simulate hypothetical transactions without touching the real one.
The central danger is the shallow-clone trap. A shallow copy duplicates the top-level object but shares references to inner mutable objects, so if the account holds a list of transactions, the copy's list is the same list. Adding a transaction to the copy also adds it to the original. A deep copy fixes this by recursively copying inner mutable objects.
Joshua Bloch's criticism of Cloneable is standard interview material. Cloneable is a marker interface with no clone method, clone() bypasses constructors and cannot assign final fields, it throws a checked exception even when cloning is valid, and it produces a shallow copy by default. For all these reasons, Rajesh prefers a copy constructor: it is explicit, type-safe, works with final fields, and deep-copy logic is visible.
The senior nuances are that for plain arrays, clone() is fine and idiomatic, and for genuinely immutable classes copying is unnecessary because sharing one instance is safe when nothing can mutate it.
Remember one sentence: prefer a copy constructor over Cloneable because it is explicit, type-safe, works with final fields, and makes the deep-copy logic visible.
---