Prototype Pattern: Cloning for Success

java java21 scala scala2 scala3 kotlin design-patterns creational-patterns prototype-pattern

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; }
}

View in repository

data class Document(
    val title: String,
    val author: String,
    val sections: MutableList<String> = mutableListOf(),
) {
    fun deepCopy(): Document = copy(sections = sections.toMutableList())
}

View in repository

case class Document(
    title: String,
    author: String,
    sections: List[String] = Nil
) {

  def addSection(section: String): Document =
    copy(sections = sections :+ section)
}

View in repository

case class Document(
    title: String,
    author: String,
    sections: List[String] = Nil
):

  def addSection(section: String): Document =
    copy(sections = sections :+ section)

View in repository

Scala Developer Mental Model

  • In Java 21, the Prototype pattern is usually tied to the Cloneable interface and Object.clone(). It works, but the contract is weak: clone() is protected, throws a checked exception, and produces a shallow copy by default. A dedicated deepCopy() method or copy constructor is usually safer.
  • In Kotlin, data class gives you a copy() method for free. It is the preferred prototype mechanism, but remember that it copies references for mutable nested state, so add a deepCopy() 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:

  1. The copy is a different object.
  2. 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());
}

View full test file

@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)
}

View full test file

@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)
}

View full test file

@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)

View full test file

When to Use the Prototype Pattern

Use the Prototype pattern when:

  1. Creating an object from scratch is expensive or complex.
  2. Objects are similar and only differ in a few fields or nested values.
  3. You want to avoid subclass explosion just to vary object state.
  4. You need to preserve a known-good configuration and branch from it.

Avoid it when:

  1. Objects are cheap to construct and state is simple.
  2. Your language gives you safer copy mechanisms (Kotlin data class, Scala case class).
  3. 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:

Test files:


Key Takeaways

  1. In Java 21, prefer a copy constructor or dedicated deepCopy() method over Cloneable. If you use clone(), remember it produces a shallow copy by default.
  2. In Kotlin, data class copy() is the idiomatic prototype. Add a deepCopy() helper when you have mutable nested state.
  3. In Scala 2/3, case classes and immutable collections make prototypes trivial and safe. The shallow vs deep distinction rarely matters.
  4. Deep copies are only necessary when nested state is mutable. If everything is immutable, a reference copy is already safe.
  5. 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.