Imagine you are building a reporting tool. Every month you need a new “Quarterly Report” document that starts with the same title, author, and boilerplate sections, but each department then adds its own custom sections. Creating the report from scratch every time is tedious and error-prone. Wouldn’t it be easier to take an existing report, copy it, and tweak the copy?
That is exactly what the Prototype pattern does: it creates new objects by copying an existing object, called the prototype, rather than building them from scratch.
In this post, we’ll implement the Prototype pattern in Java 21, Kotlin, Scala 2, and Scala 3. We’ll see why shallow copies can be dangerous, when deep copies matter, and how language features like Kotlin’s data class and Scala’s case classes make cloning far safer than Java’s Cloneable interface.
The Problem: Templates That Need Their Own Identity
Let’s say you have a Document class that contains:
- A title.
- An author.
- A list of sections.
You want to produce a new document from a template. A naive approach is to construct a fresh object each time, but if the template is complex, that becomes repetitive. The Prototype pattern lets you say: “Give me a copy of this object, and I’ll adjust it.”
The danger is that a simple copy might share mutable state with the original. If one copy adds a section, the template might unexpectedly grow too. That is the shallow vs deep copy problem.
Key Concepts
| Concept | What it means | Risk |
|---|---|---|
| Shallow copy | Copies the object but keeps references to the same nested objects | Mutating nested state leaks between copies |
| Deep copy | Recursively copies nested objects so each copy is fully independent | More code, easy to forget a field |
| Cloneable | Java’s marker interface enabling Object.clone() |
Weak contract, checked exception, hard to get right |
The Solution: Prototype Implementations Across Languages
Below is the same document prototype idea in Java 21, Kotlin, Scala 2, and Scala 3. Java shows the classic Cloneable approach plus a safer deep-copy method; the other languages rely on language-level copy mechanisms.
public class Document implements Cloneable {
private String title;
private String author;
private List<String> sections;
public Document(String title, String author, List<String> sections) {
this.title = title;
this.author = author;
this.sections = new ArrayList<>(sections);
}
@Override
public Document clone() {
try {
return (Document) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
public Document deepCopy() {
return new Document(title, author, new ArrayList<>(sections));
}
public List<String> getSections() { return sections; }
}
data class Document(
val title: String,
val author: String,
val sections: MutableList<String> = mutableListOf(),
) {
fun deepCopy(): Document = copy(sections = sections.toMutableList())
}
case class Document(
title: String,
author: String,
sections: List[String] = Nil
) {
def addSection(section: String): Document =
copy(sections = sections :+ section)
}
case class Document(
title: String,
author: String,
sections: List[String] = Nil
):
def addSection(section: String): Document =
copy(sections = sections :+ section)
Scala Developer Mental Model
- In Java 21, the Prototype pattern is usually tied to the
Cloneableinterface andObject.clone(). It works, but the contract is weak:clone()isprotected, throws a checked exception, and produces a shallow copy by default. A dedicateddeepCopy()method or copy constructor is usually safer. - In Kotlin,
data classgives you acopy()method for free. It is the preferred prototype mechanism, but remember that it copies references for mutable nested state, so add adeepCopy()helper when needed. - In Scala 2/3, case classes give you
copy()automatically. Because the default collections are immutable, the shallow vs deep distinction almost disappears: you get a new value, and the original cannot be mutated through its reference.
Shallow vs Deep Copy in Action
The snippet below shows the practical difference between a shallow clone and a deep copy in Java and Kotlin. In Scala, the same test demonstrates that copy() produces an independent value.
Document original = new Document("Report", "Ada",
new ArrayList<>(List.of("Introduction")));
Document shallow = original.clone();
shallow.getSections().add("Conclusion");
// ❌ original now also has "Conclusion"
Document deep = original.deepCopy();
deep.getSections().add("Appendix");
// ✓ original is untouched
val original = Document("Report", "Ada", mutableListOf("Introduction"))
val shallow = original.copy()
shallow.sections.add("Conclusion")
// ❌ original.sections now also has "Conclusion"
val deep = original.deepCopy()
deep.sections.add("Appendix")
// ✓ original is untouched
val original = Document("Report", "Ada", List("Introduction"))
val updated = original.addSection("Conclusion")
// ✓ original is unchanged because List is immutable
// updated.sections == List("Introduction", "Conclusion")
val original = Document("Report", "Ada", List("Introduction"))
val updated = original.addSection("Conclusion")
// ✓ original is unchanged because List is immutable
// updated.sections == List("Introduction", "Conclusion")
Comparison: Java 21 vs Scala 2 vs Scala 3 vs Kotlin
| Language | Prototype mechanism | Handles nested mutable state | Boilerplate |
|---|---|---|---|
| Java 21 | Cloneable + clone(), or copy constructor |
Manual deep copy required | High |
| Kotlin | data class copy() + custom deepCopy() |
Manual deep copy for mutable nested state | Low |
| Scala 2 | case class copy() |
Immutability makes it safe by default | Very low |
| Scala 3 | case class copy() (cleaner syntax) |
Immutability makes it safe by default | Very low |
Testing the Prototype
Prototype tests should prove two things:
- The copy is a different object.
- Mutating the copy does not affect the original (for deep copies).
@Test
@DisplayName("Deep copy should create an independent document")
void deepCopyShouldBeIndependent() {
Document original = new Document("Annual Report", "Ada",
new ArrayList<>(List.of("Introduction", "Market Analysis")));
Document deepCopy = original.deepCopy();
deepCopy.getSections().add("Conclusion");
assertNotSame(original, deepCopy);
assertEquals(2, original.getSections().size());
assertEquals(3, deepCopy.getSections().size());
}
@Test
fun deepCopyShouldBeIndependent() {
val original = Document("Annual Report", "Ada",
mutableListOf("Introduction", "Market Analysis"))
val deepCopy = original.deepCopy()
deepCopy.sections.add("Conclusion")
assertNotSame(original, deepCopy)
assertEquals(2, original.sections.size)
assertEquals(3, deepCopy.sections.size)
}
@Test
@DisplayName("copy() should create an independent document")
def copyShouldCreateIndependentDocument(): Unit = {
val original = Document("Annual Report", "Ada",
List("Introduction", "Market Analysis"))
val updated = original.addSection("Conclusion")
assertNotSame(original, updated)
assertEquals(2, original.sections.size)
assertEquals(3, updated.sections.size)
}
@Test
@DisplayName("copy() should create an independent document")
def copyShouldCreateIndependentDocument(): Unit =
val original = Document("Annual Report", "Ada",
List("Introduction", "Market Analysis"))
val updated = original.addSection("Conclusion")
assertNotSame(original, updated)
assertEquals(2, original.sections.size)
assertEquals(3, updated.sections.size)
When to Use the Prototype Pattern
Use the Prototype pattern when:
- Creating an object from scratch is expensive or complex.
- Objects are similar and only differ in a few fields or nested values.
- You want to avoid subclass explosion just to vary object state.
- You need to preserve a known-good configuration and branch from it.
Avoid it when:
- Objects are cheap to construct and state is simple.
- Your language gives you safer copy mechanisms (Kotlin
data class, Scala case class). - Deep-copy logic becomes so complex that a builder or factory is clearer.
Code Samples
All examples in this post are available in the repository:
Implementation files:
- Java 21: Document.java
- Kotlin: Document.kt
- Scala 2: Document.scala
- Scala 3: Document.scala
Test files:
- Java 21: DocumentTest.java
- Kotlin: DocumentTest.kt
- Scala 2: DocumentTest.scala
- Scala 3: DocumentTest.scala
Key Takeaways
- In Java 21, prefer a copy constructor or dedicated
deepCopy()method overCloneable. If you useclone(), remember it produces a shallow copy by default. - In Kotlin,
data classcopy()is the idiomatic prototype. Add adeepCopy()helper when you have mutable nested state. - In Scala 2/3, case classes and immutable collections make prototypes trivial and safe. The shallow vs deep distinction rarely matters.
- Deep copies are only necessary when nested state is mutable. If everything is immutable, a reference copy is already safe.
- The Prototype pattern is about saving construction cost, not about avoiding constructors entirely. Use it where it actually simplifies the code.
This is part of our Design Patterns in JVM Languages series. Check out the full design patterns guide for more patterns and interview preparation.