skip to content

When implementing a copy operation for the Prototype pattern, what is the difference between a shallow copy and a deep copy, and what bug does choosing wrongly cause?

level: middleimportance: must knowfreq 70%

answer

  1. Shallow = new box, same contents pointers
  2. Deep = recursively duplicate reachable graph
  3. Aliasing bug: edit copy, original changes
  4. Too deep: duplicated IDs, caches, listeners, cycles
  5. Per-field: mutable? owned? → copy / share / reset

basics

~20 s

A shallow copy duplicates the object's own fields, so reference fields still point at the same nested objects — original and copy share them. A deep copy also duplicates those nested objects. Choosing shallow wrongly means editing the copy silently changes the original.

solid answer

~50 s

A shallow copy allocates a new object and copies each field value verbatim. For primitives that is a genuine duplicate; for reference fields it copies the *reference*, so the original and the copy alias the same nested object. A deep copy recursively duplicates the reachable graph so the copy is fully independent. The failure mode of an accidental shallow copy is aliasing: `b = a.clone(); b.tags.add("x")` also mutates `a.tags`, producing spooky action at a distance that surfaces far from the clone site and is painful to debug. The failure mode of an indiscriminate deep copy is different: wasted time and memory, duplicated singletons/caches, broken identity (two objects claiming the same database ID), copied listeners firing twice, and infinite recursion on cyclic graphs. The correct answer is neither blanket policy but a per-field decision driven by ownership and mutability: duplicate mutable state the object owns, share immutable values, and deliberately reset or re-link everything else.

code

pseudocode · 17 lines
pseudocode
class Order {
  String id;              // identity  -> reset, not copied
  List<Line> lines;       // owned     -> deep copy
  Money total;            // immutable -> share
  Logger log;             // service   -> share
  List<Listener> hooks;   // wiring    -> reset to empty

  Order clone() {
    Order c = new Order();
    c.id    = newId();
    c.lines = lines.map(l -> l.clone());
    c.total = total;
    c.log   = log;
    c.hooks = [];
    return c;
  }
}

go deeper

for a junior

Define both terms and give the aliasing symptom: change the copy, the original changes too.

for a middle

Explain the per-field decision (mutable? owned?) and name the opposite failure — over-deep copies duplicating IDs, caches and listeners.

for a senior

Add cycles/shared references, non-copyable resources, copy-on-write and structural sharing, and insist the depth is a documented, tested contract.

for a principal

Argue for designing the mutability model so the question mostly disappears — immutable value objects, explicit ownership, aggregate boundaries — and for treating copy semantics as an API contract across teams.

### Vocabulary - **Field**: a slot of storage inside an object. Its value is either a *primitive/value* (a number, a boolean, an inline struct) or a *reference* (a pointer/handle naming another object elsewhere in memory). - **Aliasing**: two references naming the same object, so a mutation through one is observable through the other. - **Object graph**: the network you get by following every reference out of an object, then out of those objects, and so on. - **Ownership**: whether an object is conceptually responsible for the lifetime and contents of a nested object, or merely refers to something owned elsewhere. ### Shallow copy Allocate a new object of the same type; copy every field bit-for-bit. ``` original ──▶ [ name:"a" , tags:REF1 , owner:REF2 ] copy ──▶ [ name:"a" , tags:REF1 , owner:REF2 ] // same REF1, REF2 ``` Depth = 1. The copy is a distinct object, but everything it *points at* is shared. Cheap (O(fields)) and always terminates. Perfectly correct when every reference field points at something immutable or intentionally shared. ### Deep copy Copy the object *and* recursively copy everything reachable, rewiring the copy's references to the duplicates. ``` original ──▶ [ name:"a" , tags:REF1 ] ──▶ ["x","y"] copy ──▶ [ name:"a" , tags:REF3 ] ──▶ ["x","y"] // separate list ``` Depth = ∞ (or until you decide to stop). The copy is independent: no mutation of one is visible through the other. Cost is O(size of reachable graph) in both time and memory, and the naive recursion diverges on cycles. ### The bug from copying too shallowly The classic symptom: a user edits one document and another document changes. Concretely: ``` order2 = order1.clone(); // shallow order2.lines.add(newLine); // lines was shared assert order1.lines.size() == old; // FAILS ``` What makes this expensive is *distance*: nothing fails at the clone site, so the stack trace points at innocent code. It also tends to be **intermittent** — it only bites when someone eventually mutates the shared part, which may be a different feature written months later. And it is a **contract** bug, not a local one: callers reasonably assume a copy is independent, so every future caller inherits the trap. ### The bug from copying too deeply Equally real, just quieter: - **Performance/memory**: cloning a node that transitively references a 200 MB cache duplicates the cache. - **Duplicated identity**: copying a database primary key or a UUID yields two objects claiming to be the same entity; the second save either overwrites or violates a uniqueness constraint. - **Duplicated shared services**: a logger, connection pool, or configuration object that should have exactly one instance gets cloned; now half the system logs to a detached sink. - **Duplicated observers**: copied listener lists mean a single event triggers the handler twice. - **Non-copyable resources**: open sockets, file handles, threads, mutexes — copying a lock object is meaningless or actively unsafe. - **Non-termination**: `a.next = b; b.prev = a` sends naive recursion into an infinite loop or stack overflow. ### The real rule: decide per field For each field ask two questions. 1. **Is it mutable?** Immutable values (strings in most languages, frozen value objects, numbers) can always be shared — nobody can change them, so aliasing is unobservable. 2. **Does this object own it?** Owned mutable state (an internal list, a nested settings object that only I mutate) must be duplicated. Non-owned references (parent pointer, shared registry, injected service) should be shared or re-linked by the caller. That yields a third category worth naming explicitly: **fields that must be neither copied nor shared but reset** — identity/keys, timestamps like `createdAt`, version/etag counters, caches, listener lists, and anything tied to an external resource. ### Middle grounds - **Copy-on-write**: share nested state initially and duplicate it lazily on first mutation. Gives deep-copy semantics at shallow-copy cost when copies are mostly read. Requires that mutation always go through a controlled path. - **Persistent / structurally-shared data structures**: immutable collections that produce a modified version sharing most of the old one. Then "copy" is free and correct by construction. - **Partial deep copy**: deep-copy the owned subtree, share the rest — what most real `clone()` implementations actually do, and what should be documented. ### Practical guidance - Make copy depth part of the **documented contract** of the type, not an implementation detail. - **Test it**: clone, mutate every mutable part of the copy, assert the original is unchanged; and mutate the original, assert the copy is unchanged. This single test catches most aliasing regressions. - Prefer immutability where you can — the cleanest way to make the shallow/deep question disappear. - Beware container copies: copying a list usually copies the *references* it holds, i.e. a shallow copy one level down. "Copy the collection" ≠ "copy the elements".

  • Copying a list gives you a new list. Is that a deep copy of the data?
    No. It duplicates the container and the references it holds, so the elements are still shared — a shallow copy one level down. It is deep only if the elements are immutable, otherwise you must also copy each element.
  • How would you cheaply get deep-copy semantics for objects that are copied often but rarely mutated?
    Copy-on-write: share the nested state on copy and duplicate it lazily on the first mutation through a controlled setter. Or use persistent/immutable data structures with structural sharing, where a modified version shares most nodes with the original and no copying is needed at all.

Photocopying a folder. A shallow copy is a new folder holding the same sheets of paper — write on a sheet and both folders show the edit. A deep copy is a new folder with freshly photocopied sheets. But some things in the folder shouldn't be duplicated at all: the folder's registration number, and the key to the filing cabinet.

saying these in an interview costs you the question

  • "clone() always means deep copy" — most built-in clone/copy facilities are shallow by default.
  • "Deep copy is always the safe choice" — it duplicates identities, caches, listeners and non-copyable resources, and loops forever on cycles.
  • "Copying the collection copies the elements."
  • Deciding depth globally for the whole class instead of per field by mutability and ownership.
  • Assuming immutable strings/values must be duplicated for safety.
  • Shipping a copy operation with no test that mutating the copy leaves the original untouched.

context