Create new objects by copying an existing configured instance, rather than constructing one from scratch.
01
The problem Prototype solves
The problem
Your reporting service builds a ReportTemplate: it loads a layout file, parses a stylesheet, compiles a set of formatting rules and resolves fonts. About 300 ms of work, and the result is a big object graph.
Every tenant needs that same template with two fields changed — their logo and their currency. So you run all 300 ms again, four hundred times a night, to produce four hundred objects that differ in two fields.
You can't cache one shared instance, because each tenant mutates their copy. And you can't easily rebuild "just the different bits", because the expensive part is the parsing, not the two fields.
02
How the Prototype pattern works
Build one fully-configured instance, then copy it whenever you need another:
Build the expensive object once and keep it as the prototype.
Give it a clone() that returns a new instance with the same state — no parsing, no I/O, just a copy.
Callers clone and then change the handful of fields they care about.
The one decision that matters is copy depth. A shallow copy duplicates the object but shares everything it points at — so mutating clone.styles also changes the prototype's, and every other clone's. A deep copy duplicates the whole graph, which is correct but can cost as much as rebuilding. Most real prototypes are a deliberate mix: deep-copy the mutable parts, share the immutable ones.
Participants. The Prototype declares clone(). A Concrete Prototype implements it. The Client asks a prototype to copy itself and never calls a constructor — which also means it never needs to know the concrete class.
03
See it: N objects, one expensive setup
Ask for several templates. With the pattern on you get a new object every time — that's the difference from Singleton — but only the first one pays for parsing. Turn it off and every request re-parses.
▶ Try it — clone vs. rebuild
An interactive heap: cloning a prototype produces a distinct new object on every call while paying the expensive setup cost only once, whereas constructing from scratch repeats the parsing work every time.
Compare the counters with Singleton's simulator. Singleton: 1 object, 1 setup. Prototype: N objects, 1 setup. Same saving, opposite guarantee — and that's exactly why they solve different problems.
04
Prototype pattern code examples
Cloning an expensively-built template, with copy depth chosen deliberately.
public final class ReportTemplate implements Cloneable {
private final Layout layout; // immutable — safe to share
private final Map<String, Style> styles; // mutable — must be copied
private String logo;
private String currency;
public ReportTemplate(Path spec) {
this.layout = LayoutParser.parse(spec); // the expensive part
this.styles = StyleSheet.compile(spec);
}
// Copy constructor rather than Object.clone(): explicit, works with final
// fields, and doesn't drag in the Cloneable/CloneNotSupportedException mess.
private ReportTemplate(ReportTemplate other) {
this.layout = other.layout; // shared: immutable
this.styles = new HashMap<>(other.styles); // copied: mutable
this.logo = other.logo;
this.currency = other.currency;
}
public ReportTemplate copy() { return new ReportTemplate(this); }
public ReportTemplate withLogo(String logo) {
ReportTemplate c = copy();
c.logo = logo;
return c;
}
}
// ReportTemplate base = new ReportTemplate(Path.of("invoice.spec")); // once
// ReportTemplate acme = base.withLogo("acme.png"); // free
//
// Effective Java is blunt about java.lang.Cloneable: prefer a copy constructor
// or a static copy factory. Object.clone() bypasses constructors and interacts
// badly with final fields.
import copy
from dataclasses import dataclass, field, replace
@dataclass
class ReportTemplate:
layout: Layout # immutable — safe to share
styles: dict[str, Style] # mutable — must be copied
logo: str = ""
currency: str = "USD"
@classmethod
def parse(cls, spec: Path) -> "ReportTemplate":
return cls(layout=parse_layout(spec), styles=compile_styles(spec))
def clone(self, **overrides) -> "ReportTemplate":
# Choose the depth per field rather than reaching for deepcopy(self):
# layout is expensive AND immutable, so copying it would be pure waste.
return replace(
self,
styles=copy.deepcopy(self.styles),
**overrides,
)
base = ReportTemplate.parse(Path("invoice.spec")) # once
acme = base.clone(logo="acme.png") # free
# copy.copy() is shallow, copy.deepcopy() recurses the whole graph and handles
# cycles. deepcopy is correct-by-default and can be dramatically slower than
# the parse you were trying to avoid — measure before assuming it's a win.
# Customise per class with __deepcopy__ when it matters.
class ReportTemplate {
public:
static ReportTemplate parse(const std::filesystem::path& spec) {
return ReportTemplate(parse_layout(spec), compile_styles(spec));
}
// Virtual clone ("virtual constructor"): callers holding a base pointer can
// copy the real derived type without knowing what it is.
[[nodiscard]] virtual std::unique_ptr<ReportTemplate> clone() const {
return std::make_unique<ReportTemplate>(*this);
}
virtual ~ReportTemplate() = default;
private:
// shared_ptr to a const layout: every clone shares the expensive immutable
// graph, and the copy constructor only has to bump a refcount.
std::shared_ptr<const Layout> layout_;
std::map<std::string, Style> styles_; // value member -> deep-copied for free
std::string logo_, currency_;
};
// The default copy constructor already does the right thing here, because the
// members were chosen to have the right copy semantics: shared_ptr shares,
// std::map copies. Getting the member types right is most of the work.
public sealed class ReportTemplate
{
public Layout Layout { get; private init; } = null!; // immutable, shared
public Dictionary<string, Style> Styles { get; private init; } = new();
public string Logo { get; init; } = "";
public string Currency { get; init; } = "USD";
public static ReportTemplate Parse(string spec) => new()
{
Layout = LayoutParser.Parse(spec), // the expensive part
Styles = StyleSheet.Compile(spec),
};
public ReportTemplate Clone() => new()
{
Layout = Layout, // shared
Styles = new Dictionary<string, Style>(Styles), // copied
Logo = Logo,
Currency = Currency,
};
}
// If ReportTemplate were a `record`, `with` gives you a SHALLOW copy for free:
//
// var acme = baseTemplate with { Logo = "acme.png" };
//
// Convenient and fast — but note that Styles would then be the SAME dictionary
// in both, so mutating the clone's styles mutates the prototype's.
// MemberwiseClone() has exactly the same caveat.
class ReportTemplate {
static parse(spec) {
const t = new ReportTemplate()
t.layout = parseLayout(spec) // the expensive part
t.styles = compileStyles(spec)
t.logo = ''
t.currency = 'USD'
return t
}
clone(overrides = {}) {
const copy = Object.create(ReportTemplate.prototype)
copy.layout = this.layout // shared: immutable
copy.styles = structuredClone(this.styles) // copied: mutable
return Object.assign(copy, { logo: this.logo, currency: this.currency }, overrides)
}
}
const base = ReportTemplate.parse('invoice.spec') // once
const acme = base.clone({ logo: 'acme.png' }) // free
// Three copy tools, three depths:
// { ...obj } shallow — nested objects are SHARED
// structuredClone(obj) deep, handles cycles, but drops functions and class
// identity (the result is a plain object)
// JSON.parse(JSON.stringify(obj)) deep-ish — silently destroys Date, Map,
// Set, undefined and BigInt. Avoid.
//
// JavaScript is the one language here that is prototypal all the way down:
// Object.create(proto) is this pattern built into the object model.
package report
// Go has no clone() convention and no copy constructors, so this is explicit —
// which is arguably the honest version, since the copy depth is right there.
type Template struct {
Layout *Layout // immutable after parse — safe to share
Styles map[string]Style // reference type: MUST be copied by hand
Logo string
Currency string
}
func Parse(spec string) (*Template, error) {
layout, err := parseLayout(spec) // the expensive part
if err != nil {
return nil, err
}
return &Template{Layout: layout, Styles: compileStyles(spec), Currency: "USD"}, nil
}
// Clone returns a copy that is safe to mutate independently.
func (t *Template) Clone() *Template {
cp := *t // struct copy: cheap, but maps/slices/pointers are still SHARED
cp.Styles = make(map[string]Style, len(t.Styles))
for k, v := range t.Styles {
cp.Styles[k] = v
}
return &cp
}
// base, _ := report.Parse("invoice.spec") // once
// acme := base.Clone(); acme.Logo = "acme.png"
//
// `cp := *t` is the trap: it looks like a full copy and compiles fine, but
// every map, slice, channel and pointer field is still shared with the
// original. maps.Clone and slices.Clone (Go 1.21+) handle one level each.
interface Style { readonly font: string; readonly size: number }
export class ReportTemplate {
private constructor(
readonly layout: Layout,
readonly styles: ReadonlyMap<string, Style>,
readonly logo = '',
readonly currency = 'USD',
) {}
static parse(spec: string): ReportTemplate {
return new ReportTemplate(parseLayout(spec), compileStyles(spec))
}
// Partial<> types the overrides, so `clone({ logoo: 'x' })` is a compile
// error rather than a silently ignored property.
clone(overrides: Partial<Pick<ReportTemplate, 'logo' | 'currency'>> = {}): ReportTemplate {
return new ReportTemplate(
this.layout, // shared: immutable
new Map(this.styles), // copied: one level is enough here,
// because Style itself is readonly
overrides.logo ?? this.logo,
overrides.currency ?? this.currency,
)
}
}
// TypeScript's real contribution to this pattern is `readonly`: once a field is
// genuinely immutable in the type system, you can share it between clones with
// confidence instead of deep-copying it defensively.
Read across the tabs and one theme dominates: every language gives you a cheap copy that looks complete and isn't. Go's cp := *t, C#'s with and MemberwiseClone(), JavaScript's spread, Python's copy.copy — all shallow, all compile, all share their nested state. The languages that make Prototype pleasant are the ones where you can mark the shareable parts immutable (readonly, shared_ptr<const T>) and then only copy what's left.
05
How to implement Prototype
Confirm the copy is actually cheaper than the construction. If the object is a big mutable graph, a deep copy can cost more than re-parsing — measure before you commit.
Classify every field: immutable (share it), mutable (copy it), identity (a database id or timestamp that must not be copied).
Prefer a copy constructor or a clone() you wrote by hand over the language's magic clone. Explicit depth is the whole point.
Return a new instance rather than mutating; combine with with-style overrides so callers can clone-and-change in one call.
Make the prototype itself effectively immutable, or every clone inherits whichever mutation happened most recently.
In a class hierarchy, make clone() virtual and return the concrete type, so cloning through a base reference produces the right subclass.
06
When to use Prototype — and when not to
Use it when construction is genuinely expensive (parsing, I/O, compilation) and most of the result is identical between instances; when you need many near-identical objects that each get mutated; when you must copy an object whose concrete class you don't know; or when the object's configuration comes from user actions and can't be re-derived from a constructor.
Where it goes wrong
The accidental shallow copy. The clone works in the test, ships, and then two tenants start seeing each other's styles because they share a mutable map. This is the defining bug of this pattern and it never shows up at compile time.
Deep copy that costs more than construction.copy.deepcopy() on a large graph can be slower than the parse you were avoiding — and it will happily copy the 40 MB immutable layout you meant to share.
Cloned identity. Copying an object that carries a database primary key, a UUID or a created-at timestamp produces two objects claiming to be the same row. Decide explicitly which fields must be reset.
Cycles and unclonable members. Object graphs with cycles need a visited-set, and members like open sockets, file handles or locks cannot meaningfully be copied at all.
Factory picks a class to instantiate; Prototype avoids instantiation entirely.
07
Quick check
🧠 Quick check
A Template has a Map<String, Style> styles. Its clone() copies every field across directly. A tenant edits one style on their clone. What happens?
Assigning a map, list, array or object reference copies the reference, not the contents — so every clone points at the prototype's single map. This is the shallow-copy trap, and it's the defining failure mode of Prototype in every language on the code tabs: Go's cp := *t, C#'s with, JavaScript's spread and Python's copy.copy all behave exactly this way. The fix is to copy mutable members explicitly and share only what is genuinely immutable.
In the wild
JavaScriptObject.create(proto) — JavaScript's object model is prototypal, so this pattern is the language rather than a pattern in it.
Pythoncopy.copy() / copy.deepcopy() plus the __copy__ and __deepcopy__ hooks for per-class control.
JavaObject.clone() and Cloneable — the cautionary tale. Effective Java recommends copy constructors instead.
C#record types with with expressions, and MemberwiseClone() — both shallow by design.
Gomaps.Clone and slices.Clone (Go 1.21+), plus proto.Clone in Protocol Buffers.
C++The virtual-clone idiom, used throughout LLVM and Qt (QObject-style hierarchies) to copy through a base pointer.
What is the difference between a shallow copy and a deep copy?
A shallow copy duplicates the object's own fields, so any field holding a reference — a list, map, array or object — ends up pointing at the same nested data as the original. A deep copy recursively duplicates the whole graph, so the two are fully independent. Shallow is fast and usually the default ({...obj}, with, MemberwiseClone, cp := *t); deep is correct but can be slower than simply rebuilding the object.
When should I use Prototype instead of a constructor?
When construction is genuinely expensive and most of its result is identical between instances — parsing a spec, compiling a template, loading reference data — or when the object's configuration was assembled at runtime and can't be re-derived from constructor arguments. If construction is cheap, a constructor is clearer and Prototype adds a maintenance burden with no payoff.
Why does Effective Java advise against Cloneable?
Object.clone() creates the copy without running any constructor, which breaks invariants that constructors are supposed to enforce and interacts badly with final fields. Cloneable also doesn't declare clone(), so it's a marker interface that changes the behaviour of a protected method — an unusual and confusing design. A copy constructor or static copy factory does the same job explicitly, works with final fields, and can return an interface type.
What's the difference between Prototype and Flyweight?
Both avoid repeating expensive work, in opposite ways. Prototypecopies, giving you N independent objects that each cost almost nothing after the first. Flyweightshares, giving you one object referenced from many places. Choose Prototype when each copy will be mutated; Flyweight when the state is genuinely identical and read-only, since sharing then beats copying.
Finish the handbook, earn the certificate
All 23 Gang-of-Four patterns, each with a simulator you can click and code in seven languages. Free, no signup.