StudyDSA logoStudyDSA

Command Palette

Search for a command to run...

Sign InSign Up
Sign Up

Data StructuresAlgorithmsBig-O NotationDesign PatternsSystem DesignMachine LearningPhysicsRoboticsAI Research

Factory MethodAbstract FactoryBuilderPrototypeSingletonAdapterBridgeCompositeDecoratorFacadeFlyweightProxyChain of ResponsibilityCommandIteratorMediatorMementoObserverStateStrategyTemplate MethodVisitor

Definition
StudyDSA

Where complexity meets clarity.
By Armas Zarra.

Topics

  • Data Structures
  • Algorithms
  • Big-O Notation
  • Robotics
  • AI Research
  • Machine Learning

Practice

  • Blind 75
  • LeetCode 75
  • NeetCode 150

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Armas Films LLC

Design PatternsPrototype

Prototype

A creational pattern where objects copy themselves through a clone() method, so new instances come from configured examples instead of constructors.

Definition

Try to duplicate an object you hold behind an interface and new abandons you immediately: you can't construct what you can't name, and the variable's type tells you Shape, not which shape. Prototype is the Gang of Four creational pattern that makes objects responsible for copying themselves, so new instances come from existing ones instead of from constructors. The catalog entry boils down to one move: keep a configured instance around and make new objects by cloning it.


A prototype is really just an object with a virtual clone() method. Virtual is the load-bearing word, because dispatch sends the call to the object's actual class, the one place in the program that knows every field and can copy them all. The same move pays off when construction is expensive: build the costly thing once, then stamp copies instead of re-running the setup.

Copying What You Can't Name

Here's the pattern at its smallest. Shape declares clone() alongside its normal operations, Circle implements it with its own copy constructor call, and the duplicate() function works on any Shape without naming one. The example clones a circle at (10, 20) and moves the copy to x = 99. Before reading past the code, predict both describe() outputs:

The original still reports circle r=5 at (10, 20) and the copy reports circle r=5 at (99, 20), two independent objects from one clone() call. The detail worth a second look is duplicate(): it copies a circle correctly while containing no mention of circles. Add a Square class tomorrow and duplicate() copies squares too, unedited, because the class-specific knowledge lives inside each class's own clone().

The Shallow Copy Trap

Now the part that pages people at 2am. A copy is shallow when it duplicates the object's fields but shares whatever those fields point at, and shallow is what you get by default almost everywhere: Java's Object.clone() documents itself as copying fields without cloning their contents, and Python's copy module makes the split explicit by shipping copy.copy() and copy.deepcopy() as separate functions. The document below clones both ways. Predict what original.tags prints after the shallow clone appends "draft":

The original prints ['finance', 'draft']. It changed, because the shallow clone copied the Document object while sharing the one tags list between both documents, so the append landed in a list the original also holds. The deep clone's append leaves the original alone. What counts as shallow shifts by language, and the code tabs above carry the differences: Go slices share their backing array through a struct copy, C++ copies members by value so the trap needs a pointer field to appear, and Rust's derive(Clone) copies owned data deeply, so sharing has to be opted into with Rc.

Who Does What

The cast is the smallest in the creational family: a Prototype interface declaring clone(), ConcretePrototypes that implement it (Circle), and a Client that copies through the interface (duplicate()). One optional role earns its keep in practice: a prototype registry, a plain map from names to configured exemplar objects. Code asks the registry for "invoice" and gets a clone of the stored exemplar, which turns object variants into data instead of subclasses.


The pattern works like a photocopier: feed it any page and it reproduces the page without understanding a word of the content, the way duplicate() copies shapes it can't name. The analogy breaks exactly where the last section did. Paper is one layer deep, but objects have depth, fields pointing at other objects, and the copier has no opinion on whether to duplicate those or share them. That decision is yours per field, and the shallow default most languages hand you is an answer chosen by omission.

Prototype: Step-by-Step

Installing the pattern is four moves:

1. Declare the Copy on the Interface

Shape declares clone() right next to the operations everyone already uses. That placement is the pattern: copying becomes something you can ask of any shape you hold, not just the ones you can name.

2. Let Each Class Copy Itself

Circle.clone() returns Circle(self.radius, self.x, self.y), and only Circle could write that line, because only it sees its own fields. The knowledge of how to copy lives with the thing being copied.

3. Decide Shallow or Deep, Field by Field

Primitives copy cleanly, but every reference field is a decision: share it or duplicate it. A clone that shares a mutable list is two objects wearing one set of guts, and that decision deserves to be made on purpose rather than inherited from a default.

4. Keep Configured Exemplars in a Registry

When variants are configuration rather than code, store one fully set-up instance per variant in a map and serve registry["invoice"].clone() on demand. New variants become registry entries instead of new classes.
The Go and Rust Versions

Go expresses the pattern with a struct dereference. copied := *c copies every field by value in one line, and wrapping that in a Clone() Shape method makes it polymorphic. The honesty cost is the slice: a Go slice is a small header pointing at a backing array, so the one-line copy duplicates the header and shares the array, which is the shallow trap with Go-specific spelling. Deep copies of slices and maps are written by hand with make and copy, and the language ships no general deep copy at all.


Rust turned the pattern into a one-line derive. The Clone trait generates a member-wise copy, and because Vec and String clone their contents, the derived copy is deep wherever data is owned. Sharing can't happen by accident: it requires reaching for Rc visibly in the type. One wrinkle matters for the GoF shape: Clone isn't object-safe, so copying through a trait object takes an explicit clone_box() method like the one in the code block, a small tax for dispatching the copy at runtime.

Prototype vs Copy Constructor

You might think clone() is a copy constructor with extra ceremony, and at a single call site with a known class, it is: Circle(original) says the same thing more directly. The difference is who knows the class. A copy constructor must be invoked by name, so the caller commits to Circle at compile time. A clone() call dispatches on the object's runtime type, which is why duplicate() can copy shapes it has never heard of. One is a tool for call sites that know, the other for call sites that can't.


Java's built-in attempt is the cautionary tale here. Object.clone() arrives protected, requires implementing the empty Cloneable marker interface to not throw CloneNotSupportedException, and still hands back a shallow copy. Java style guides have steered people toward copy constructors and static factory copies instead for years, which is a useful reminder that the pattern is the idea, not any particular language's built-in for it.

Best Practices

The method is short and the failure modes are quiet, so the discipline is mostly about what clone() doesn't say:


The method is three lines and the bugs live in what those lines skip. For each field holding a list, map, or object, decide on purpose whether the copy shares or duplicates it, and write the decision down in the method body where reviewers can see it.

Check Yourself

Two questions before you go. First: clone() and a copy constructor both produce copies, so what can duplicate(shape) do that Circle(original) can't, and what does the caller have to lack for that difference to matter? Second: your shallow clone shares its tags list with the original. Name two ways to fix it and the cost each one carries. Both answers are on this page, one per code block.


Then go spot it in the wild. Python ships the pattern as the copy module, where __copy__ and __deepcopy__ are clone() by other names. JavaScript added structuredClone() as a built-in deep copy that even survives circular references. Last, find a class in your own codebase with a duplicate() or copy() helper and audit it field by field, asking of each list and nested object: copied, or shared? If nobody can answer from the code, that's the bug waiting.

prototype.py

from abc import ABC, abstractmethod

class Shape(ABC):  # Prototype: declares the copy operation
    @abstractmethod
    def clone(self) -> "Shape": ...

    @abstractmethod
    def move_to(self, x: int, y: int) -> None: ...

    @abstractmethod
    def describe(self) -> str: ...

class Circle(Shape):  # ConcretePrototype: knows how to copy itself
    def __init__(self, radius: int, x: int, y: int):
        self.radius = radius
        self.x = x
        self.y = y

    def clone(self) -> "Shape":
        return Circle(self.radius, self.x, self.y)

    def move_to(self, x: int, y: int) -> None:
        self.x = x
        self.y = y

    def describe(self) -> str:
        return f"circle r={self.radius} at ({self.x}, {self.y})"

def duplicate(shape: Shape) -> Shape:
    # No Circle mentioned: the object knows how to copy itself
    return shape.clone()

# Example usage
original = Circle(5, 10, 20)
copy = duplicate(original)
copy.move_to(99, 20)
print(original.describe())  # circle r=5 at (10, 20)
print(copy.describe())      # circle r=5 at (99, 20)

shallow_vs_deep_copy.py

import copy

class Document:
    def __init__(self, title: str, tags: list[str]):
        self.title = title
        self.tags = tags

    def clone_shallow(self) -> "Document":
        # Copies the object, but SHARES the tags list
        return copy.copy(self)

    def clone_deep(self) -> "Document":
        # Recursively copies the tags list too
        return copy.deepcopy(self)

# Example usage
original = Document("Q3 Report", ["finance"])

shallow = original.clone_shallow()
shallow.tags.append("draft")
print(original.tags)  # ['finance', 'draft']  <- the original changed

deep = original.clone_deep()
deep.tags.append("internal")
print(original.tags)  # ['finance', 'draft']  <- unchanged this time