June 17, 2026 in patterns5 minutes
A intermediate-level guide to Prototype: before-and-after java code and diagrams for a CS student.
When you first learn to create objects using the new keyword (the instantiation process), it seems simple. You call a constructor, pass in some values, and you have an object. However, as systems grow, creating an object can become “heavy.” This happens when an object requires complex setup logic, many parameters, or expensive data fetching before it is ready to use.
Consider a scenario where we need to create multiple levels for a game. Each level has a name, a list of enemy types, and a specific difficulty setting. If every time we want a new version of a level, we have to manually pass in all these details again, we risk errors and repetitive code.
import java.util.*;
class GameLevel {
private final String name;
private final List<String> enemyTypes;
private final int difficulty;
// Creating a new level requires many parameters and complex setup logic
public GameLevel(String name, List<String> enemyTypes, int difficulty) {
this.name = name;
this.enemyTypes = enemyTypes;
this.difficulty = difficulty;
}
@Override
public String toString() {
return "Level: " + name + ", Enemies: " + enemyTypes + ", Difficulty: " + difficulty;
}
}The problem here is that the client (the part of the program using this class) must know exactly how to construct a GameLevel from scratch every single time. If the construction process becomes more complex—perhaps involving database calls or heavy computations—the client is forced to manage all that complexity.
classDiagram
class GameLevel {
-String name
-List enemyTypes
-int difficulty
+GameLevel(name, enemyTypes, difficulty)
}
class Client {
+createLevels()
}
Client ..> GameLevel : creates via constructor
Think of a complex document you spent hours typing and formatting. If you need five copies of it to distribute to teammates, you do not re-type the entire document five times. Instead, you put the original in a photocopy machine and press “copy.”
The Prototype pattern applies this exact logic to software. Rather than using new to build an object from the ground up, you take an existing “prototype” object and ask it to produce a duplicate of itself.
To implement this, we move the responsibility of creation away from the client and into the object itself. We define a common interface that specifies how an object should copy itself.
import java.util.*;
interface Prototype<T> {
T copy();
}
class GameLevel implements Prototype<GameLevel> {
private String name;
private List<String> enemyTypes;
private int difficulty;
public GameLevel(String name, List<String> enemyTypes, int difficulty) {
this.name = name;
this.enemyTypes = new ArrayList<>(enemyTypes);
this.difficulty = difficulty;
}
@Override
public GameLevel copy() {
// Deep copy: we create a new list so the clone doesn't share enemy lists with the original
return new GameLevel(this.name, this.enemyTypes, this.difficulty);
}
public void setName(String name) { this.name = name; }
@Override
public String toString() {
return "Level: " + name + ", Enemies: " + enemyTypes + ", Difficulty: " + difficulty;
}
}By implementing the Prototype interface, GameLevel now provides a copy() method. The client no longer needs to know how to build a level; it only needs to know which existing level it wants to duplicate.
sequenceDiagram
participant C as Client
participant P as GameLevel (Prototype)
C ->> P: copy()
P ->> P: new GameLevel(...)
P -->> C: returns new instance
A critical concept when duplicating objects is the difference between a “shallow copy” and a “deep copy.” This is where many developers encounter bugs.
In a shallow copy, only the top-level fields are copied. If one of those fields is a reference (a pointer to another object, like an ArrayList), both the original and the copy will point to the exact same list in memory. Changing the enemies in the clone would accidentally change the enemies in the original.
In a deep copy, we also duplicate any objects held by the original. In our solution, notice that when we create the new GameLevel, we pass a new instance of an ArrayList containing the existing enemy types. This ensures the two levels are truly independent.
flowchart TD
subgraph ShallowCopy [Shallow Copy: Shared Reference]
direction LR
S1[Original Level] --> L[Shared List: Enemy Types]
S2[Cloned Level] --> L
end
subgraph DeepCopy [Deep Copy: Independent Data]
direction LR
D1[Original Level] --> L1[List A]
D2[Cloned Level] --> L2[List B]
end
The Prototype pattern is a creational design pattern. Unlike the Factory Method, which focuses on how classes are structured to delegate creation to subclasses, Prototype focuses on duplicating existing instances.
Use Prototype when:
Avoid Prototype when:
new is already extremely efficient.Using copy() to duplicate a heavy object and modify only specific parts.
GameLevel base = new GameLevel("Forest", List.of("Orc", "Goblin"), 5);
GameLevel clone = base.copy();
clone.setName("Dark Forest");
System.out.println(base); // Level: Forest, Enemies: [Orc, Goblin], Difficulty: 5
System.out.println(clone); // Level: Dark Forest, Enemies: [Orc, Goblin], Difficulty: 5