intermediate~2h

Prototype Pattern

Learn how the Prototype pattern lets an object clone itself instead of forcing client code to rebuild it field by field, and why the shallow-copy versus deep-copy decision is the whole point of the pattern.

Learning objectives

  • Explain why letting client code manually reconstruct an object is a coupling problem
  • Implement a generic Prototype interface and use it to clone an object graph
  • Tell shallow copy and deep copy apart, and choose correctly per field
  • Recognize where Java's built-in Cloneable mechanism is easy to misuse

◆ Story

A study group treats one member's fully worked-out notes as a master copy. Instead of everyone starting from a blank page and writing the same definitions and diagrams from scratch, each person photocopies the master and edits their own copy from there. The expensive part — actually working everything out the first time — happens exactly once, and every copy after that is nearly free.

Software objects can behave the same way. When something is expensive or fiddly to build — a fully populated configuration object, a mid-game board, a document that already has real content in it — it's often cheaper to duplicate an existing instance than to reconstruct one from raw inputs every time. The Prototype pattern turns that idea into a concrete technique: instead of always calling a constructor and re-filling every field, you ask an existing object to hand you a copy of itself.

This chapter builds that idea around a small board game engine. A GameBoard holds a list of GamePiece objects, each with a color and a position on the board. At various points during play, the game needs to save a snapshot of the current board — for undo/redo, or for periodic checkpointing — while the original board keeps changing as play continues, completely unaffected by whatever happens to the snapshot.

◆ The problem

Saving a checkpoint sounds simple enough: build a second GameBoard, and copy every piece into it. The obvious way to do that is to have the calling code walk the original board's list of pieces and construct brand-new GamePiece objects itself, reading each field out one at a time.

That code below does work — the resulting copiedBoard is genuinely independent of board, and moving a piece on one doesn't touch the other. But look closely at what the client had to know to make that happen: it had to call getColor() and getPosition() by name, meaning it had to understand GamePiece's entire internal shape just to produce a copy of one.

That's the real issue. The client's job is to play the game, not to know how to correctly reconstruct a GamePiece or a GameBoard field by field. Every place in the codebase that ever needs a copy of a board would have to repeat this same field-by-field logic — and the moment GamePiece grows a third field, say a pieceType distinguishing a player's piece from an opponent's, every one of those copy sites needs to be found and updated, even though none of them actually care about pieceType for any other reason. The class that changed is GamePiece; the code that has to change is scattered everywhere copying happens. That mismatch is the mistake worth fixing.

💻 Code example

class GamePiece { private String color; private int position; GamePiece(String color, int position) { this.color = color; this.position = position; } String getColor() { return color; } int getPosition() { return position; } void setPosition(int position) { this.position = position; } } class GameBoard { private final List<GamePiece> pieces = new ArrayList<>(); void addPiece(GamePiece piece) { pieces.add(piece); } List<GamePiece> getPieces() { return pieces; } } // the client, building a "copy" by hand GameBoard board = new GameBoard(); board.addPiece(new GamePiece("red", 1)); board.addPiece(new GamePiece("blue", 5)); GameBoard copiedBoard = new GameBoard(); for (GamePiece piece : board.getPieces()) { // the client has to know GamePiece's exact fields to copy one copiedBoard.addPiece(new GamePiece(piece.getColor(), piece.getPosition())); }

Whose job should copying really be? Not the client's — the client only wants "a copy," not the mechanics of producing one correctly. The class being copied is the only thing that genuinely knows its own complete, current set of fields, so it should be the one responsible for copying itself.

That reasoning leads to three decisions. First, every class that needs to be copyable should own a method that knows how to copy itself. Second, there should be one shared name for that operation across every class, so any part of the codebase that wants a copy of something can ask for it the same way regardless of what that something is. Third, a container object's own copy logic should ask each of its parts to copy themselves, rather than reaching into their fields directly — GameBoard shouldn't need to know what fields GamePiece has any more than the client did.

That's the Prototype pattern: give each class its own clone() method, so an entire object graph — a board and every piece on it — can be copied without any outside code needing to understand or reconstruct its internal structure. A shared generic interface makes the contract explicit: anything that implements Prototype<T> promises it knows how to hand back a T that's a copy of itself. GamePiece implements it directly, since it's the piece that actually holds the fields. GameBoard implements it too, but its clone() doesn't touch a single GamePiece field — it just asks each piece to clone itself and collects the results. That delegation is the whole point: if GamePiece gains a pieceType field tomorrow, exactly one method needs updating — GamePiece.clone() — and GameBoard never notices.

💻 Code example

interface Prototype<T> { T clone(); } class GamePiece implements Prototype<GamePiece> { private String color; private int position; GamePiece(String color, int position) { this.color = color; this.position = position; } String getColor() { return color; } int getPosition() { return position; } void setPosition(int position) { this.position = position; } public GamePiece clone() { // GamePiece is the only class that ever needs to know its own fields return new GamePiece(this.color, this.position); } } class GameBoard implements Prototype<GameBoard> { private final List<GamePiece> pieces = new ArrayList<>(); void addPiece(GamePiece piece) { pieces.add(piece); } List<GamePiece> getPieces() { return pieces; } public GameBoard clone() { GameBoard newBoard = new GameBoard(); for (GamePiece piece : this.pieces) { newBoard.addPiece(piece.clone()); // delegates -- never reads a GamePiece field directly } return newBoard; } }

With both clone() methods in place, taking a checkpoint collapses to a single line — the client doesn't know or care how the copy actually happens underneath. Moving a piece on the original board afterward leaves the checkpoint's pieces exactly where they were, because GameBoard.clone() built the checkpoint out of entirely new GamePiece objects, not references to the originals.

This is also where the pattern proves its real value: adding a new field to GamePiece doesn't ripple anywhere. Suppose pieceType gets added to distinguish a pawn from a king. The only method that needs to change is GamePiece.clone() itself, so it also copies pieceType into the new instance. GameBoard.clone() is untouched, and so is every other place in the codebase that calls board.clone(). Compare that to the naive version from the previous section, where that same new field would have meant hunting down every hand-written copying loop across the app.

💻 Code example

public class Main { public static void main(String[] args) { GameBoard board = new GameBoard(); board.addPiece(new GamePiece("red", 1)); board.addPiece(new GamePiece("blue", 5)); GameBoard checkpoint = board.clone(); // one line -- the client doesn't know how it happens board.getPieces().get(0).setPosition(3); // mutate the original System.out.println(checkpoint.getPieces().get(0).getPosition()); // still 1 -- genuinely independent } }

▲ Edge case — shallow copy vs. deep copy

If GameBoard.clone() had instead written newBoard.addPiece(piece) — adding the original piece object directly instead of calling piece.clone() — both boards would end up holding references to the exact same GamePiece objects. Moving a piece on the checkpoint would then silently move it on the original board too, since there was only ever one GamePiece object the whole time, just referenced from two lists. That's a shallow copy: the container gets duplicated, but its contents are shared. Calling piece.clone() for every piece — a deep copy — is what actually gives the checkpoint independent GamePiece objects. The rule that generalizes: any field that's itself a mutable object needs to be explicitly, recursively cloned; only genuinely immutable fields, like a String or a primitive, are safe to copy by simply assigning the same value.

▲ Edge case — Java's Cloneable is easy to get wrong

Java ships a built-in Object.clone() mechanism behind the Cloneable marker interface, but its default behavior is a shallow copy of every field, and forgetting to override it correctly for a mutable field is a common source of subtle bugs. A hand-written Prototype interface with an explicit clone() method forces that deep-vs-shallow decision to be made deliberately, field by field, rather than inherited silently from a default you didn't ask for.

▲ Edge case — deep-copying everything isn't automatically the right default

Not every field benefits from a deep copy. If GamePiece held a reference to some genuinely shared, immutable configuration object — a rule set every piece on the board legitimately shares and never mutates — deep-copying it on every clone would be wasted work for no safety benefit. The decision should be made per field, based on whether that field is actually mutable and actually meant to be independent after cloning, not applied as a blanket rule across the whole class.

◆ Where this shows up

Game state checkpointing and undo/redo are the exact scenario this chapter builds, and they're a genuinely common real use case in any application that needs to snapshot mutable state and roll back to it later.

Cloning is also a cheap way to spawn many similar objects — game enemies or particles that share a mostly-fixed configuration are often built once as a fully set-up prototype, then cloned repeatedly instead of re-parsing configuration or re-running expensive setup for every new instance. Document and object templates work the same way: a pre-configured "starter" object gets cloned and lightly customized for each new use, rather than assembled from scratch every time.

Java's own Object.clone() and the Cloneable marker interface are the built-in version of this pattern, though as the edge cases above cover, its default shallow-copy behavior means it needs to be overridden carefully for any class with mutable fields — which is exactly why many real codebases write their own explicit clone() methods instead of relying on it directly.

Q: What problem does the Prototype pattern actually solve? A: It moves the responsibility for copying an object out of client code and into the object itself — each class knows how to clone itself, so callers and container classes never need to know or reconstruct another class's internal fields.

Q: Why does GameBoard.clone() call piece.clone() instead of copying each GamePiece's fields directly? A: So GameBoard never needs to know GamePiece's internal structure. If GamePiece gains a new field later, only GamePiece.clone() needs to change.

Q: What real bug does a shallow copy risk? A: Cloned objects can end up sharing the same underlying mutable fields as the original, so mutating one silently mutates the other — avoided by explicitly deep-copying every mutable reference field inside clone().

Q: Does every field need to be deep-copied? A: No. Only fields that are both mutable and meant to be independent after cloning. Immutable or intentionally shared fields are safe to copy by value or reference without cloning them.

Q: Why do many teams avoid Java's built-in Cloneable? A: Object.clone() defaults to a shallow copy of every field, and it's easy to forget to override that correctly for mutable fields — a hand-written clone() method forces the deep-vs-shallow decision to be made deliberately instead.

Want a visual for this concept?

Generate a diagram tailored to “Prototype Pattern” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Adapter Pattern← Back to all Low-Level Design & Design Patterns chapters