Most explanations of the Prototype pattern start and end with "it lets you clone objects." That tells you what it does, not why you'd ever want it. The real reason has nothing to do with cloning for its own sake — it's about the expensive work that goes into building an object in the first place.
The benefit, stated plainly
Sometimes building an object is expensive. Not the object itself — the work required to fill it. A heavy computation. A slow database read. Three network calls to outside services. A scoring model that takes two seconds to run. And the result of all that work gets stored inside the object.
Now imagine you need a second object that is almost the same. Do you redo the two seconds of work? Or do you take the one you already built — which already has the expensive result inside it — and copy it?
That is the Prototype pattern. When building an object involves heavy work whose result is stored in the object, you pay that cost once, then copy the finished object instead of rebuilding from scratch. The second object, and the third, and the tenth, come almost for free.
Where the cost actually hurts
Let me show the expensive part honestly, because a toy example with an empty constructor hides the whole point.
class CreditReport {
String customerId;
int score;
List<String> riskFactors;
CreditReport(String customerId) {
this.customerId = customerId;
// EXPENSIVE: pulls from three credit bureaus and runs a scoring
// model. Takes about two seconds every time it runs.
this.score = runExpensiveScoringModel(customerId);
this.riskFactors = fetchFromCreditBureaus(customerId);
}
}
Suppose you need five "what-if" variations of one customer's report — same base data, each with one number nudged for a scenario.
Without the Prototype pattern
Every variation calls the constructor, so the two-second work runs every single time:
CreditReport base = new CreditReport("CUST-001"); // ~2 seconds
CreditReport scenarioA = new CreditReport("CUST-001"); // ~2 seconds AGAIN
CreditReport scenarioB = new CreditReport("CUST-001"); // ~2 seconds AGAIN
CreditReport scenarioC = new CreditReport("CUST-001"); // ~2 seconds AGAIN
CreditReport scenarioD = new CreditReport("CUST-001"); // ~2 seconds AGAIN
scenarioA.score += 20;
scenarioB.score -= 50;
scenarioC.score += 10;
scenarioD.score -= 30;
Five objects, roughly ten seconds — and almost all of it spent recomputing a result you already had after the first call.
With the Prototype pattern
Give the object a copy() method that carries the already-computed result over, and skip the expensive constructor for every copy:
class CreditReport {
String customerId;
int score;
List<String> riskFactors;
CreditReport(String customerId) {
this.customerId = customerId;
this.score = runExpensiveScoringModel(customerId); // the ~2s work
this.riskFactors = fetchFromCreditBureaus(customerId);
}
// A second, private constructor that does NO expensive work.
private CreditReport() { }
// The photocopier: hand back an independent copy, expensive result included.
CreditReport copy() {
CreditReport c = new CreditReport(); // no bureau calls, no model run
c.customerId = this.customerId;
c.score = this.score; // reuse the computed score
c.riskFactors = new ArrayList<>(this.riskFactors); // (note this line)
return c;
}
}
CreditReport base = new CreditReport("CUST-001"); // ~2 seconds, paid ONCE
CreditReport scenarioA = base.copy(); scenarioA.score += 20; // instant
CreditReport scenarioB = base.copy(); scenarioB.score -= 50; // instant
CreditReport scenarioC = base.copy(); scenarioC.score += 10; // instant
CreditReport scenarioD = base.copy(); scenarioD.score -= 30; // instant
About two seconds total instead of ten. The expensive work is already sitting inside base, so each copy just carries it along. That is the prize.
The honest part — copying isn't free
Be clear about one thing: the lines inside copy() do run, every time.
c.customerId = this.customerId;
c.score = this.score;
c.riskFactors = new ArrayList<>(this.riskFactors);
These cost CPU. The new ArrayList<>(...) line actually allocates a fresh list and copies its elements in. Prototype does not make objects appear out of nothing.
The point is the comparison. These field copies are trivial — fractions of a microsecond — next to the two-second bureau pull and model run you are avoiding. You are not getting copies for free; you are trading a tiny, cheap copy for a huge, expensive rebuild. When the rebuild is genuinely expensive, that trade is a bargain. When the constructor is cheap and does nothing, this trade buys you nothing at all — so don't bother with Prototype there.
The trap that bites everyone — shallow vs deep copy
Look again at that one line:
c.riskFactors = new ArrayList<>(this.riskFactors); // a NEW list
That new ArrayList<>(...) matters enormously. Suppose you'd written the lazy version instead:
c.riskFactors = this.riskFactors; // the SAME list object
Now every copy points at the same underlying list. So this happens:
scenarioA.riskFactors.add("late-payment-2024");
// scenarioB, scenarioC, and base now ALL show that risk factor too.
You thought you photocopied the report, but all the copies were secretly stapled to one shared page — write on it through any copy and everyone changes. That is the difference between a shallow copy (duplicates the top object but shares the nested objects inside it) and a deep copy (duplicates the nested objects too, so each copy is truly independent).
The real work of Prototype isn't the cloning itself — it's making the copy deep enough that copies don't secretly share their mutable parts. For one list inside, that's a single careful line. For a deeply nested object, it's genuinely fiddly, and it's the main reason hand-rolled cloning earns a bad reputation.
A small Java note: prefer a plain copy() method or a copy constructor like new CreditReport(base) over implementing the old Cloneable interface — it's clearer about exactly what gets copied, and you stay in control of the shallow-versus-deep decision.
When to use it — and when not to
Reach for Prototype when at least one of these is true:
- Building the object is genuinely expensive — heavy computation, a database or network read, lots of validation — and you'd rather pay that once.
- You need many objects that are mostly identical, with small variations each.
- You want to snapshot an object's current state and branch new variations from it.
Skip it when objects are cheap and simple to build. If new Point(x, y) is the whole story, a copy method is just indirection that hides nothing. The pattern earns its place only when construction is costly, or the near-identical-copies problem is real.
The one sentence to remember
Prototype is a photocopier: when building an object takes expensive work whose result is stored inside it, build one, then copy it instead of rebuilding from scratch — and the only catch is making the copy deep enough that the copies don't secretly share their parts.
Top comments (0)