You have a complex Document object: 50 fields loaded from a database, nested objects, pre-computed formatting. A user requests a “duplicate” of it with one field changed. Rebuilding it from scratch through the database query path is expensive. Re-running a complex initialization sequence just to get a near-identical copy is wasteful. The Prototype pattern says: copy the existing object instead. Start with what you have, change what you need.
This guide builds the Prototype pattern from the problem it solves, explains the critical difference between shallow and deep copying, covers Java’s Cloneable pitfalls, and shows the preferred copy constructor alternative. By the end you will know how to copy objects safely and which approach to use in modern Java code.
Tested with Java 21. All examples compile and run with no external dependencies.
The Problem: Expensive Object Creation
Some objects are expensive to create — not because of complex constructor logic, but because initialization requires external resources: a database query, a network call, a file read, or a lengthy computation. Once you have one of these objects in memory, creating a near-identical variant by re-running the initialization is wasteful.
A second scenario: you want to create multiple variations of an object that all share the same base configuration. Instead of setting the same 30 fields on each, you create one configured “prototype” and clone it for each variation. The Prototype pattern is the formal mechanism for both scenarios.
What Is the Prototype Pattern?
The Prototype is a creational design pattern. It specifies the kind of objects to create using a prototypical instance, and creates new objects by copying this prototype.
“Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.” — Design Patterns, Gamma et al.
The pattern has one key operation: clone. Any object that participates in the pattern must know how to copy itself. The client asks the object to clone itself; the object knows all its internal structure and produces a correct copy. The client does not need to know the object’s class to copy it.
The Roles — Mapped to Our Example
| GoF Term | In Our Example | What It Does |
|---|---|---|
| Prototype | Copyable interface | Declares the copy() method. Any class that can be cloned implements this. |
| Concrete Prototype | Document | Implements copy() to produce a correct copy of itself, including all nested state. |
| Client | Code calling original.copy() | Asks an existing object to clone itself. Does not use new Document() to create variants. |
How the UML Diagram Maps to Our Code

Copyable interface. “ConcretePrototype” = our Document. The Client calls clone()/copy() on a Prototype reference without knowing the concrete class. Diagram credit: Refactoring.GuruThe Client’s arrow points to the Prototype interface, not to any concrete class. This is the power: the client can clone objects it does not know the runtime type of — useful in plugin architectures, shape editors, and configuration systems where object types are registered at runtime.
Shallow Copy vs. Deep Copy: The Most Important Distinction
Before writing any code, you must understand the difference between shallow and deep copying. Getting this wrong is the most common Prototype bug.
Imagine a Document that has a List<String> tags field. A shallow copy copies the List reference — both the original and the copy point to the same list object. Add a tag to the copy and it appears in the original too. A deep copy creates a new List and copies all the elements — the original and the copy have independent lists.
import java.util.ArrayList;
import java.util.List;
public class ShallowVsDeepDemo {
static class ShallowDocument {
String title;
List<String> tags; // mutable reference
ShallowDocument(String title, List<String> tags) {
this.title = title;
this.tags = tags;
}
// Shallow copy: copies the reference, not the list itself
ShallowDocument shallowCopy() {
return new ShallowDocument(this.title, this.tags); // same list!
}
// Deep copy: creates a new list with the same contents
ShallowDocument deepCopy() {
return new ShallowDocument(this.title, new ArrayList<>(this.tags));
}
}
public static void main(String[] args) {
ShallowDocument original = new ShallowDocument("Report", new ArrayList<>(List.of("draft")));
ShallowDocument shallow = original.shallowCopy();
shallow.tags.add("reviewed"); // Modifies original.tags too!
System.out.println("Original tags: " + original.tags); // [draft, reviewed] — BUG!
// Reset
original = new ShallowDocument("Report", new ArrayList<>(List.of("draft")));
ShallowDocument deep = original.deepCopy();
deep.tags.add("reviewed"); // Only affects the copy
System.out.println("Original tags: " + original.tags); // [draft] — correct
System.out.println("Copy tags: " + deep.tags); // [draft, reviewed]
}
}
The shallow copy looks correct because both documents share the same data — until a mutation reveals they are not independent. Any time your object holds a mutable field (a List, Map, Date, StringBuilder, or any object with setters), you need a deep copy of that field. Immutable fields (String, Integer, LocalDate) are safe to share without copying.
⚠️ Watch Out: A shallow copy is not wrong by definition — it is correct when all shared fields are immutable. The bug only appears when a mutable field is shared. The rule: for each mutable field, decide whether to share (shallow) or duplicate (deep) based on whether you want the copy to be independent.
Building the Pattern: Full Document Example
Part 1 — The Prototype Interface: Copyable
We define a clean Copyable interface instead of using Java’s built-in Cloneable (more on why below). The interface declares one method: copy(), which returns the same type as the implementing class.
// Our Prototype interface: any class that can clone itself implements this.
// The return type T allows each implementing class to return its own type,
// not the raw interface type, which makes the client code cleaner.
public interface Copyable<T> {
T copy();
}
Part 2 — The Nested Object: DocumentMetadata
Our Document contains a nested DocumentMetadata object. To deep-copy a Document correctly, the nested object must also be copyable. This is the nested copy problem: if Document.copy() copies the metadata reference instead of the metadata object, mutations to the copy’s metadata affect the original.
import java.time.LocalDate;
public class DocumentMetadata implements Copyable<DocumentMetadata> {
private String author;
private LocalDate createdDate; // LocalDate is immutable — safe to share
private String version;
public DocumentMetadata(String author, LocalDate createdDate, String version) {
this.author = author;
this.createdDate = createdDate; // safe: LocalDate is immutable
this.version = version;
}
// Copy constructor: creates a new instance with the same values
public DocumentMetadata(DocumentMetadata source) {
this.author = source.author; // String: immutable, safe to share
this.createdDate = source.createdDate; // LocalDate: immutable, safe to share
this.version = source.version;
}
@Override
public DocumentMetadata copy() {
return new DocumentMetadata(this); // delegates to copy constructor
}
public String getAuthor() { return author; }
public LocalDate getCreatedDate(){ return createdDate; }
public String getVersion() { return version; }
public void setVersion(String v) { this.version = v; }
public void setAuthor(String a) { this.author = a; }
@Override public String toString() {
return String.format("Metadata{author='%s', date=%s, version='%s'}",
author, createdDate, version);
}
}
Part 3 — The Concrete Prototype: Document
The Document class implements Copyable. Its copy() method performs a deep copy: the nested DocumentMetadata is copied (not shared), and the List<String> is duplicated into a new list. String fields (title, content) are immutable and safe to share.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Document implements Copyable<Document> {
private String title; // String: immutable, safe to share on copy
private String content; // String: immutable, safe to share on copy
private List<String> tags; // List: mutable, MUST be deep-copied
private DocumentMetadata metadata; // Mutable object, MUST be deep-copied
public Document(String title, String content, List<String> tags, DocumentMetadata metadata) {
this.title = title;
this.content = content;
this.tags = new ArrayList<>(tags); // defensive copy on construction
this.metadata = metadata;
}
// Copy constructor: the preferred Prototype implementation in modern Java
public Document(Document source) {
this.title = source.title; // immutable: share
this.content = source.content; // immutable: share
this.tags = new ArrayList<>(source.tags); // mutable: new list
this.metadata = source.metadata.copy(); // mutable: deep copy via Copyable
}
@Override
public Document copy() {
return new Document(this); // delegates to copy constructor
}
// Getters and setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getContent() { return content; }
public void setContent(String content) { this.content = content; }
public List<String> getTags() { return Collections.unmodifiableList(tags); }
public void addTag(String tag) { tags.add(tag); }
public DocumentMetadata getMetadata() { return metadata; }
@Override public String toString() {
return String.format("Document{title='%s', tags=%s, %s}", title, tags, metadata);
}
}
Read the copy constructor carefully: title and content are Strings — immutable, safe to assign directly. tags is a List — mutable, so we create new ArrayList<>(source.tags) to get an independent copy. metadata is a mutable object — we call source.metadata.copy() to get a deep copy of that nested object too. Each field requires an explicit decision.
Part 4 — Using the Prototype: Client Code
import java.time.LocalDate;
import java.util.List;
public class Main {
public static void main(String[] args) {
// Create the original document (imagine this was loaded from a database)
DocumentMetadata metadata = new DocumentMetadata("Alice", LocalDate.of(2025, 1, 15), "1.0");
Document original = new Document(
"Q1 Report",
"Revenue increased by 12%...",
List.of("finance", "quarterly"),
metadata
);
System.out.println("Original: " + original);
// Clone the document and apply only the differences
Document draft = original.copy();
draft.setTitle("Q1 Report — DRAFT");
draft.addTag("draft");
draft.getMetadata().setAuthor("Bob"); // only the copy's metadata changes
draft.getMetadata().setVersion("1.0-draft");
System.out.println("Draft: " + draft);
System.out.println("Original: " + original); // unchanged — deep copy worked
// Verify independence
System.out.println("nSame object? " + (original == draft)); // false
System.out.println("Same tags? " + (original.getTags() == draft.getTags())); // false
System.out.println("Same meta? " + (original.getMetadata() == draft.getMetadata())); // false
}
}
The output confirms independence: the original title, tags, and metadata are unchanged after we modify the draft. The deep copy strategy — new list, cloned metadata — keeps the two documents isolated. This is the behavior you must verify in tests whenever you implement copy().
Java’s Cloneable Interface: Why Most Code Avoids It
Object.clone() and the Cloneable marker interface are Java’s built-in mechanism for copying objects. In practice, they have four well-known problems that lead most teams to prefer copy constructors instead.
// What Cloneable looks like
public class Document implements Cloneable {
private List<String> tags;
@Override
public Document clone() {
try {
Document cloned = (Document) super.clone(); // shallow copy by default
cloned.tags = new ArrayList<>(this.tags); // must manually deep-copy each mutable field
return cloned;
} catch (CloneNotSupportedException e) {
// This can never happen when the class implements Cloneable,
// but the compiler forces you to catch it anyway.
throw new AssertionError("This cannot happen", e);
}
}
}
The four problems: First, Cloneable is a marker interface with no methods — it does not guarantee a clone() method is public or even accessible. Second, super.clone() always produces a shallow copy; you must manually deep-copy every mutable field with no compiler help. Third, the forced try/catch for an exception that can never actually be thrown is pure noise. Fourth, if a superclass is not designed for cloning, calling super.clone() may produce incorrect results. Joshua Bloch’s verdict in Effective Java: “The Cloneable interface has many problems; its use should be avoided.” Prefer copy constructors.
✅ Best Practice: Implement the Prototype pattern using a copy constructor (public MyClass(MyClass source) { ... }) and expose it via an interface method like copy(). Avoid Cloneable and Object.clone() in new code.
Prototype Registry: Managing a Catalog of Prototypes
A common extension is the Prototype Registry — a map of named prototypes that clients can clone by name without knowing the concrete class. This is useful in template or preset systems where you want to offer a catalog of starting configurations.
import java.util.HashMap;
import java.util.Map;
public class DocumentRegistry {
private final Map<String, Document> prototypes = new HashMap<>();
// Register a prototype under a name
public void register(String name, Document prototype) {
prototypes.put(name, prototype);
}
// Return a fresh copy of the named prototype
public Document get(String name) {
Document prototype = prototypes.get(name);
if (prototype == null) throw new IllegalArgumentException("No prototype: " + name);
return prototype.copy(); // always return a copy, never the prototype itself
}
}
// Setup:
DocumentRegistry registry = new DocumentRegistry();
DocumentMetadata meta = new DocumentMetadata("Template", LocalDate.now(), "1.0");
registry.register("blank-report",
new Document("Untitled Report", "", List.of("report"), meta));
registry.register("q-report-template",
new Document("Quarterly Report Template", "## Executive Summaryn...",
List.of("quarterly", "finance"), meta));
// Usage: each caller gets an independent copy
Document myReport = registry.get("q-report-template");
myReport.setTitle("Q2 2025 Report");
myReport.addTag("q2");
The registry holds the prototypes internally; clients get clones. The original prototypes in the registry are never modified. This pattern appears in game engines (item templates), reporting tools (document templates), and configuration systems (infrastructure presets).
When to Use Prototype — and When to Skip It
Reach for Prototype when: Object initialization is expensive (database load, network call, complex computation) and you need multiple similar instances. You need to create objects without knowing their exact class at compile time (plugin architectures, runtime type registration). You want to offer a “template” or “preset” system where users start from a known configuration and customize it.
Skip it when: Object initialization is cheap — a plain new call is simpler and equally fast. The object is immutable — sharing the original is safe and no copy is needed. The “copy” is actually a different object type — use a Builder or Factory instead. All fields are mutable and deeply nested — the deep copy logic becomes complex enough to be its own correctness problem.
💡 Key Insight: The Prototype pattern delegates copying responsibility to the object itself. This matters when the object’s internals are complex or partially encapsulated — only the object knows which fields need deep copying and which are safe to share. Centralizing that knowledge in the object is better than distributing it across every client that ever needs a copy.
Frequently Asked Questions
Is Java serialization a way to deep-copy objects?
Technically yes — serializing to a byte array and deserializing produces a deep copy. In practice this is discouraged: it requires Serializable, it is significantly slower than a copy constructor, it can fail on nested objects that are not serializable, and it bypasses constructor validation. Use it only as a last resort when you cannot modify the class to add a copy constructor.
How does Prototype differ from Builder?
Builder assembles a new object step by step from scratch, specifying each field explicitly. Prototype starts with an existing object and copies it, then applies only the differences. Use Builder when you are constructing from zero; use Prototype when you are diverging from a known good state. They work well together: a Builder can take an existing object as a starting point (essentially a copy constructor), and a Prototype copy can then be modified through a builder-style API.
When is a shallow copy correct?
A shallow copy is correct when all shared fields are immutable. If every mutable reference in your object points to an immutable type (String, Integer, LocalDate, an unmodifiable list), a shallow copy produces a fully independent object because the shared data cannot be changed. The problem only arises with mutable shared state.
5 AI Prompts for Prototype
- “Review this class and tell me which fields require deep copying and which are safe to share in a shallow copy. Explain your reasoning for each field. [paste class]”
- “I have a class that implements Cloneable. Refactor it to use a copy constructor instead, removing all use of Object.clone(). Show before and after, and explain each change.”
- “Write a JUnit 5 test for a copy() method that verifies: (1) the returned object is not the same reference, (2) mutable fields are independent copies, and (3) the original is unchanged after mutating the copy.”
- “Show me a Prototype Registry in Java that stores named template objects and returns clones. Include thread safety using ConcurrentHashMap and explain why the cloning must happen at get() time, not registration time.”
- “My object has three levels of nesting: Order contains LineItems which contain ProductDetails. Show me how to implement deep copy correctly at each level using copy constructors, and explain what breaks if I use shallow copy at the LineItem level.”
Conclusion
The Prototype pattern is fundamentally about delegation: instead of re-running an expensive initialization, you ask an existing object to copy itself. The object knows its own structure — which fields are safe to share and which must be independently duplicated — so it is the right place to own the copy logic.
The critical practice: for every mutable field in your class, make an explicit decision at copy time — share or duplicate. Document that decision in a comment. Write a test that verifies independence. Shallow copies look correct until a mutation reveals they are not; deep copy correctness should be verified, not assumed.
See Also
- Builder Design Pattern in Java — constructing objects from scratch when a starting prototype is not available
- Factory Method Design Pattern in Java — deciding which type to create when cloning the existing type is not appropriate
- Singleton Design Pattern in Java — the opposite of Prototype: one instance, never copied
Further Reading
- Prototype — Refactoring.Guru
- Prototype Pattern in Java — Baeldung
- Effective Java, 3rd Edition — Joshua Bloch, Item 13 (Override clone judiciously) — explains why Cloneable is broken and copy constructors are preferred
- Design Patterns: Elements of Reusable Object-Oriented Software — Gamma, Helm, Johnson, Vlissides, Chapter 3