Prototype
Part of the free Low-Level Design course on LogicWiz, in the chapter: Creational patterns. The class-diagram exercises run in the app.
Prototype: copy an order without sharing its insides
"I tapped Reorder and changed the milk from 2 to 5. Now last week's order in my history also says 5."
A SwiftKart tester, in a bug report
The problem: a copy that is not a copy
SwiftKart has a Reorder button. A customer opens an old order and taps it. The app builds a new draft order with the same items, address, delivery slot and rider note. A draft order is an order that is still being edited and has not been placed yet. The customer may change it before paying.
A DraftOrder holds an id, a slot, a note, an Address and a list of LineItem objects. A LineItem holds a SKU (the code of one product) and a quantity. The quantity can be changed. Arjun wrote the copy by hand:
public class ReorderService {
public DraftOrder reorder(DraftOrder old, String newId) {
DraftOrder copy = new DraftOrder(newId, old.slot());
copy.setAddress(old.address());
copy.setNote(old.note());
for (LineItem item : old.lines()) {
copy.addLine(item); // the same LineItem object, now in two orders
}
return copy;
}
}
// again = reorder(lastWeek, "ord-102"); again.lines().get(0).setQuantity(5);
// lastWeek.lines().get(0).quantity() now prints 5, not 2
Arjun said, "But I made a new order and a new list. The two orders have separate lists, so they are separate copies." Priya answered, "The lists are two. The items inside them are one." Each list holds a reference to the same LineItem. A reference is the arrow from a variable or a list to an object. Copying the arrow does not copy the object.
When two references point to one object, that is aliasing. A change through one reference shows through the other. Even the plainest line does it:
DraftOrder alias = lastWeek; // no new object at all, only a second name
alias.setId("ord-101"); // lastWeek.id() now returns "ord-101"
So there are three depths of copying:
- Reference copy: no new object. Two names, one order.
- Shallow copy: a new outer object, but its fields point to the same inner objects as the original.
- Deep copy: a new outer object and new inner objects, as far down as anything can be changed.
Arjun's reorder made a shallow copy. A second problem is also waiting. Another service copies the cart for the "weekly staples" template, and it has its own copy of this code. When Arjun added the note field last month, he updated one service and forgot the other.
The idea: photocopy a filled-in form
Think of a filled-in order form. You photocopy it, cross out the milk on the copy and write a new number. The original stays as it was, because the copy is new paper.
Now suppose the form says "see attached sheet 3". The photocopier copies those words, but not sheet 3. Both forms point to the same sheet, and a change to that sheet shows on both. That is a shallow copy. To get a deep copy, you must copy the attached sheets too.
In code, the answer is to let each object copy itself, because it knows its own fields. The caller says "copy yourself" and then makes small changes to the result. The object that is copied is the prototype, and the pattern is called Prototype: create a new object by copying an existing one instead of building it from nothing.
The photocopier is blind. It copies everything, including the serial number printed on the form. In code you decide, field by field, what a copy means. A copy of an order should not keep the old order id.
The structure
classDiagram
class Prototype {
<<interface>>
+copy() Prototype
}
Prototype <|.. DraftOrder
DraftOrder <|-- ScheduledDraftOrder
TemplateRegistry --> Prototype : keeps named prototypes
Client ..> TemplateRegistry : asks for a copy by name
Client ..> Prototype : asks any prototype to copy itself
The roles:
- Prototype: the promise that an object can copy itself. In Java the interface is often skipped, and
copy()sits on the class. - Concrete prototype: a class that knows how to copy itself, such as
DraftOrder. - Client: code that needs a new object and asks a prototype for a copy.
- Registry (optional): named prototypes the client copies, such as "weekly staples".
The real gain is this: the client calls copy() on a variable of the interface type, and the right class comes back without the client knowing it.
Build it step by step
Step 1: Java's built-in way, and why people avoid it. Java has Object.clone(). A class opts in by implementing Cloneable.
public class DraftOrder implements Cloneable {
private String id;
private final List<LineItem> lines = new ArrayList<>();
// constructor, getters and addLine omitted
@Override
public DraftOrder clone() {
try {
return (DraftOrder) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // cannot happen: we implement Cloneable
}
}
}
// again = lastWeek.clone(); again.addLine(bread);
// lastWeek.lines().size() is now 2, and lastWeek.lines() == again.lines() is true
super.clone() copies every field as it is, so the list reference is copied and the list is shared. That is a shallow copy again. Fixing it means writing c.lines = new ArrayList<>(lines) inside clone(). The compiler refuses with "cannot assign a value to final variable lines". You must drop final, and that gives up a guarantee for the whole class.
Many Java developers avoid clone() for reasons beyond this:
Cloneablehas no methods. It only changes howObject.clone()behaves, which is an odd use of an interface.clone()creates the object without running a constructor, so the checks in your constructor are skipped.super.clone()throws a checked exception that cannot happen for your class, so every override that calls it carries atryblock.- Each subclass must override it and call
super.clone()correctly, or the copy has the wrong type.
Effective Java, a well-known Java book, advises a copy constructor or a copy factory instead. Arrays are the one place where clone() is normal. An interviewer who asks about Prototype in Java is often listening for this.
Step 2: a copy constructor. It is a constructor that takes another object of the same class and copies its fields. You decide each field.
public DraftOrder(DraftOrder other) {
this.id = other.id;
this.slot = other.slot;
this.note = other.note;
this.address = other.address; // a record: nobody can change it, so share it
for (LineItem item : other.lines) {
this.lines.add(new LineItem(item)); // a new item, not the same one
}
}
public DraftOrder copy() { return new DraftOrder(this); }
// LineItem gets the same kind of constructor: this(other.sku, other.quantity)
Ask one question per field: can anyone change what it points to? A String cannot change, and Address is a record, so both are shared safely. The list and its items can change, so each gets new objects. The list stays final, and the constructor still runs.
Now the bug is gone:
DraftOrder again = lastWeek.copy();
again.setId("ord-101");
again.lines().get(0).setQuantity(5);
again.addLine(new LineItem("BREAD", 1));
// last week: id ord-100, milk 2, lines 1
// again: id ord-101, milk 5, lines 2
A copy factory does the same with a static method, such as DraftOrder.copyOf(other). Choose either. Whichever you choose, the copy keeps the old id until the caller sets a new one.
Step 3: copy without knowing the class, and a registry. SwiftKart's "weekly staples" is a draft order that also repeats every Sunday. It is a subclass with one more field.
public class ScheduledDraftOrder extends DraftOrder {
private String repeatsOn;
public ScheduledDraftOrder(ScheduledDraftOrder other) {
super(other);
this.repeatsOn = other.repeatsOn;
}
@Override
public ScheduledDraftOrder copy() { return new ScheduledDraftOrder(this); }
}
If a client holds a DraftOrder variable, copy() still returns a ScheduledDraftOrder. The constructor new DraftOrder(x) could never do that, because the caller must name the class. This is where the pattern earns its name. The trap is a subclass that forgets to override copy(). It silently returns the parent type and loses repeatsOn. Write one test per subclass.
A registry keeps named prototypes. Marketing adds "weekly staples" and "party pack" without writing a class for each.
public DraftOrder create(String name) {
DraftOrder prototype = templates.get(name);
if (prototype == null) {
throw new IllegalArgumentException("No template named " + name);
}
return prototype.copy(); // hand out a copy, never the original
}
// register() also stores prototype.copy(), so later edits to the caller's object do not leak in
Arjun's first version returned templates.get(name) directly. The first customer added dal to "weekly staples", and the template then had two lines for every later customer. Run with the copy, the template stays at one line.
Step 4: when the parts are immutable, there is little to copy. Make LineItem a record and the deep copy disappears.
public record LineItem(String sku, int quantity) {
public LineItem withQuantity(int newQuantity) {
return new LineItem(sku, newQuantity);
}
}
// in the copy constructor: this.lines.addAll(other.lines); // safe, no item can change
// again.lines().set(0, again.lines().get(0).withQuantity(5));
// lastWeek's milk prints 2, again's prints 5
A change now means putting a new item in the list. Nothing is shared in a way that can hurt. Be careful with Collections.unmodifiableList(list): it wraps the same list, so it is another alias, not a copy. List.copyOf(list) makes a real copy.
When to use it, when not to, and what it costs
Use a prototype when:
- the object has many parts, and a new one is the old one with small changes (reorder, templates, duplicate a shape in a drawing app);
- the caller should not need to know the concrete class;
- named presets should be data in a registry, not a new class each.
Do not use it when:
- the object is immutable or a record. Share it, or make a changed version with a method like
withQuantity; - a constructor or a builder is already short and clear;
- the object owns something unique, such as an open connection, a file, a thread or a lock. Two objects cannot own one connection;
- the structure is a graph with cycles. A deep copy must remember what it has copied, or it loops or duplicates parts.
Do not choose it for speed. Java creates small objects cheaply, and a deep copy costs about as much as building the object. It pays when building is slow, such as reading a template from a database, because a copy in memory avoids the second read. Measure before you claim a gain.
The costs: every new field must be added to the copy constructor, and every subclass must override copy(). This is the same trap as copying by hand, but now it lives in one class, next to the fields.
| Pattern | Starts from | Gives you |
|---|---|---|
| Builder | nothing: parts, one by one | a new object, built to the end |
| Factory method | a request; a subclass picks the class | a new object of the chosen class |
| Prototype | an existing, finished object | an independent copy, then small changes |
| Singleton | the class itself | the same single object every time |
In interviews, Prototype shows up as duplicating a shape in a drawing editor, spawning game units from a template, repeating a booking or an expense, and email or notification templates.
Try it: draw the copy design
The starter has two services that each copy a draft order by hand. Add one interface for objects that can copy themselves, let the draft order and its line items implement it, and add a registry of named prototypes.
📌 Summary: A prototype creates a new object by copying an existing one. Decide per field whether to share or copy, prefer a copy constructor to clone(), and skip it for immutables.
What Arjun learns next
Arjun can now create objects cleanly. Next, he looks inside RazorpayCharger and PayUCharger. The two SDKs they call have different shapes, and vendor code cannot be changed. Making them fit one interface is the job of the adapter.
Check your understanding
- Arjun copies a draft order with `new ArrayList<>(old.lines())`, then calls `setQuantity(5)` on the first item of the copy. What happens to last week's order?
- An interviewer asks why many Java developers prefer a copy constructor to `clone()`. Which answer is correct?
- After one customer adds dal to a "weekly staples" template order, every later customer's new order also holds dal. What is the most likely cause?
- Which object needs no prototype or copy method at all?