What Is a Copy-on-Write Array?
Copy-on-Write (COW) is an optimization strategy that defers the expensive operation of copying data until a mutation actually occurs. When you "copy" a COW array, instead of immediately duplicating the underlying memory buffer, the new instance simply shares the same buffer with the original. Both instances point to identical, read-only storage. Only when one of them attempts to write (mutate) the data does the system perform an actual copy of the buffer, giving the mutating instance its own independent version.
This pattern sits at the intersection of value semantics and reference performance. You get the safety and predictability of value typesβmodifying one instance never accidentally affects anotherβwhile avoiding the O(n) cost of copying large collections on every assignment.
Why Copy-on-Write Matters in Technical Interviews
π Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Interviewers love COW questions because they test multiple skills simultaneously:
- Memory management understanding β reference counting, shared ownership, avoiding retain cycles
- Performance analysis β amortized cost calculations, identifying when copies actually happen
- Data structure design β separating interface from storage, designing for mutability triggers
- Concurrency awareness β thread-safety of shared buffers, atomic reference counting
- Real-world system knowledge β COW powers Swift's collections, C++ std::string implementations, Linux fork() semantics, and many file system snapshots
Candidates who can implement a COW array from scratch demonstrate they understand not just the "what" but the "why" behind language features they use daily.
How Copy-on-Write Works Under the Hood
The core mechanism relies on three components:
- A shared backing buffer that holds the actual array elements
- A reference-counting mechanism (or some uniqueness tracker) to know how many owners exist
- A "copy-on-mutation" guard that checks uniqueness before any write operation
The Shared Storage Buffer
The buffer is typically a separate heap-allocated object that both the original and the "copied" array point to. Here's a simplified representation:
// Internal storage representation (simplified)
struct SharedBuffer {
T[] data; // actual elements
int refCount; // how many CowArray instances point here
}
The "Write" Trigger
Every mutating method (set, append, remove, etc.) must first inspect the reference count. If it's greater than 1, the method performs a full copy of the buffer, decrements the old buffer's reference count, and then proceeds with the mutation on the new, uniquely-owned buffer:
// Pseudocode for every mutating operation
function ensureUniqueOwnership():
if buffer.refCount > 1:
buffer.refCount -= 1
buffer = new SharedBuffer(copyOf: buffer.data)
buffer.refCount = 1
Implementing a Copy-on-Write Array from Scratch
Let's build a complete, production-style COW array. We'll use a language-agnostic approach that translates easily to any object-oriented language.
Step 1: Define the Internal Buffer
class SharedBuffer {
T[] elements;
int refCount;
SharedBuffer(T[] initialElements) {
this.elements = initialElements;
this.refCount = 1; // starts with one owner
}
SharedBuffer clone() {
T[] copiedElements = new T[this.elements.length];
for (int i = 0; i < this.elements.length; i++) {
copiedElements[i] = this.elements[i];
}
SharedBuffer newBuffer = new SharedBuffer<>(copiedElements);
return newBuffer;
}
void retain() { this.refCount += 1; }
void release() { this.refCount -= 1; }
}
Step 2: Implement Read Operations
Read operations are fast and require no copying. They simply delegate to the shared buffer:
class CowArray {
private SharedBuffer buffer;
CowArray(T[] initialElements) {
this.buffer = new SharedBuffer<>(initialElements);
}
// Copy constructor β SHARES the buffer, does NOT copy data
CowArray(CowArray other) {
this.buffer = other.buffer;
this.buffer.retain(); // increment ref count
}
T get(int index) {
return this.buffer.elements[index]; // direct read, no copy
}
int length() {
return this.buffer.elements.length;
}
}
Step 3: Implement Write Operations with COW Logic
Every mutating method must call ensureUnique() first. This is the heart of COW:
class CowArray {
// ... previous code ...
private void ensureUnique() {
if (this.buffer.refCount > 1) {
// We share the buffer with others β must fork it
this.buffer.release(); // drop reference to shared buffer
this.buffer = this.buffer.clone(); // create private copy
this.buffer.refCount = 1; // reset count on the new buffer
}
}
void set(int index, T value) {
ensureUnique();
this.buffer.elements[index] = value;
}
void append(T value) {
ensureUnique();
T[] newElements = new T[this.buffer.elements.length + 1];
for (int i = 0; i < this.buffer.elements.length; i++) {
newElements[i] = this.buffer.elements[i];
}
newElements[this.buffer.elements.length] = value;
this.buffer.elements = newElements;
}
void remove(int index) {
ensureUnique();
T[] newElements = new T[this.buffer.elements.length - 1];
for (int i = 0, j = 0; i < this.buffer.elements.length; i++) {
if (i != index) {
newElements[j] = this.buffer.elements[i];
j++;
}
}
this.buffer.elements = newElements;
}
}
Step 4: Full Implementation with Destructor and Equality
Here is the complete, polished class ready for interview presentation:
class CowArray {
private SharedBuffer buffer;
// Constructor β creates a fresh buffer with refCount = 1
CowArray(T[] initialElements) {
this.buffer = new SharedBuffer<>(initialElements);
}
// Copy constructor β shares buffer, increments refCount
CowArray(CowArray other) {
this.buffer = other.buffer;
this.buffer.retain();
}
// Destructor / cleanup β release our reference
~CowArray() {
this.buffer.release();
// If refCount reaches 0, buffer can be deallocated
}
// --- Read operations (no copy needed) ---
T get(int index) {
if (index < 0 || index >= this.buffer.elements.length) {
throw new IndexOutOfBoundsException();
}
return this.buffer.elements[index];
}
int length() {
return this.buffer.elements.length;
}
T[] toArray() {
T[] result = new T[this.buffer.elements.length];
for (int i = 0; i < this.buffer.elements.length; i++) {
result[i] = this.buffer.elements[i];
}
return result;
}
// --- The COW guard ---
private void ensureUnique() {
if (this.buffer.refCount > 1) {
this.buffer.release();
this.buffer = this.buffer.clone();
this.buffer.refCount = 1;
}
}
// --- Mutating operations (trigger COW) ---
void set(int index, T value) {
if (index < 0 || index >= this.buffer.elements.length) {
throw new IndexOutOfBoundsException();
}
ensureUnique();
this.buffer.elements[index] = value;
}
void append(T value) {
ensureUnique();
T[] newElements = new T[this.buffer.elements.length + 1];
for (int i = 0; i < this.buffer.elements.length; i++) {
newElements[i] = this.buffer.elements[i];
}
newElements[this.buffer.elements.length] = value;
this.buffer.elements = newElements;
}
void insert(int index, T value) {
if (index < 0 || index > this.buffer.elements.length) {
throw new IndexOutOfBoundsException();
}
ensureUnique();
T[] newElements = new T[this.buffer.elements.length + 1];
for (int i = 0; i < index; i++) {
newElements[i] = this.buffer.elements[i];
}
newElements[index] = value;
for (int i = index; i < this.buffer.elements.length; i++) {
newElements[i + 1] = this.buffer.elements[i];
}
this.buffer.elements = newElements;
}
void remove(int index) {
if (index < 0 || index >= this.buffer.elements.length) {
throw new IndexOutOfBoundsException();
}
ensureUnique();
T[] newElements = new T[this.buffer.elements.length - 1];
for (int i = 0, j = 0; i < this.buffer.elements.length; i++) {
if (i != index) {
newElements[j] = this.buffer.elements[i];
j++;
}
}
this.buffer.elements = newElements;
}
// Equality: two CowArrays are equal if they share the SAME buffer
bool sharesBufferWith(CowArray other) {
return this.buffer == other.buffer;
}
// Deep equality: compare contents regardless of buffer identity
bool contentEquals(CowArray other) {
if (this.buffer.elements.length != other.buffer.elements.length) {
return false;
}
for (int i = 0; i < this.buffer.elements.length; i++) {
if (this.buffer.elements[i] != other.buffer.elements[i]) {
return false;
}
}
return true;
}
}
Common Interview Problems and Solutions
Problem 1: Demonstrate COW Behavior with a Trace
Prompt: Given the CowArray implementation above, trace the reference counts and identify exactly when physical copies occur in the following code:
CowArray a = new CowArray<>(new int[]{1, 2, 3}); // buffer refCount = 1
CowArray b = new CowArray<>(a); // buffer refCount = 2
CowArray c = new CowArray<>(b); // buffer refCount = 3
a.set(0, 99); // triggers COW for a
b.get(1); // read-only, no copy
c.append(4); // triggers COW for c
Solution: Walk through each line explaining the reference count changes. The key insight: after a.set(0, 99), a has its own buffer (refCount=1), while b and c still share the original buffer (refCount=2). When c.append(4) executes, c forks off its own buffer, leaving b as the sole owner of the original (refCount=1).
// Step-by-step trace:
// Line 1: a created -> Buffer1 { [1,2,3], refCount=1 }
// Line 2: b shares with a -> Buffer1 refCount=2
// Line 3: c shares with a,b -> Buffer1 refCount=3
// Line 4: a.set() -> ensureUnique() sees refCount=3 > 1
// -> Buffer1 refCount drops to 2 (a releases)
// -> Buffer2 cloned from Buffer1, refCount=1, owned by a
// -> Buffer2 modified: [99,2,3]
// Now: a -> Buffer2 (refCount=1), b,c -> Buffer1 (refCount=2)
// Line 5: b.get(1) -> reads Buffer1 directly, no copy
// Line 6: c.append() -> ensureUnique() sees refCount=2 > 1
// -> Buffer1 refCount drops to 1 (c releases, only b remains)
// -> Buffer3 cloned from Buffer1, refCount=1, owned by c
// -> Buffer3 gets [1,2,3,4]
// Final state:
// a -> Buffer2: [99,2,3] refCount=1
// b -> Buffer1: [1,2,3] refCount=1
// c -> Buffer3: [1,2,3,4] refCount=1
Problem 2: COW with Nested Structures (Deep vs Shallow COW)
Prompt: Your CowArray holds reference-type elements (e.g., objects, nested arrays). A shallow buffer copy duplicates pointers but not the objects themselves. How would you extend the implementation to support deep copy-on-write for nested structures?
Solution: Introduce a protocol/interface Cowable that nested types must implement. When cloning the buffer, check if elements are themselves COW-aware and clone them recursively:
interface Cowable {
T cowClone(); // returns a copy-on-write clone of itself
}
class SharedBuffer {
T[] elements;
int refCount;
SharedBuffer clone() {
T[] copiedElements = new T[this.elements.length];
for (int i = 0; i < this.elements.length; i++) {
T elem = this.elements[i];
if (elem instanceof Cowable) {
// Deep clone the nested COW structure
copiedElements[i] = ((Cowable) elem).cowClone();
} else {
// For primitives or non-COW types, shallow copy is fine
copiedElements[i] = elem;
}
}
SharedBuffer newBuffer = new SharedBuffer<>(copiedElements);
return newBuffer;
}
}
This ensures that when the outer array forks its buffer, any nested COW arrays also get their own independent buffers, preventing accidental shared mutable state at deeper levels.
Problem 3: Thread-Safe COW Array
Prompt: The basic implementation has a race condition: two threads could simultaneously call ensureUnique(), both see refCount > 1, and both create clones β resulting in lost mutations. Design a thread-safe version.
Solution: Use a mutex or atomic operations on the reference count. Here's a version using an atomic reference count and a lock per buffer:
class ThreadSafeCowArray {
private AtomicSharedBuffer buffer;
// Constructor
ThreadSafeCowArray(T[] initialElements) {
this.buffer = new AtomicSharedBuffer<>(initialElements);
}
// Copy constructor β atomically increment refCount
ThreadSafeCowArray(ThreadSafeCowArray other) {
this.buffer = other.buffer;
this.buffer.atomicRetain();
}
private void ensureUnique() {
int currentRefs = this.buffer.getRefCount();
if (currentRefs > 1) {
synchronized (this.buffer.getLock()) {
// Double-check inside lock
if (this.buffer.getRefCount() > 1) {
this.buffer.atomicRelease();
this.buffer = this.buffer.cloneUnderLock();
// New buffer starts with refCount = 1
}
}
}
}
T get(int index) {
// Reads are safe without locks on immutable shared buffer
return this.buffer.elements[index];
}
void set(int index, T value) {
ensureUnique();
// Now we have exclusive ownership β safe to mutate
this.buffer.elements[index] = value;
}
}
The critical insight: reads remain lock-free because the shared buffer is effectively immutable. Only the fork operation requires synchronization, and the double-check pattern minimizes contention.
Problem 4: Detecting Unintended COW Performance Degradation
Prompt: In a large codebase, a CowArray is accidentally causing O(n) copies inside a tight loop because of an overlooked mutation. How would you instrument the class to detect and debug this?
class InstrumentedCowArray extends CowArray {
private long copyCount = 0;
private long totalCopyTimeNs = 0;
private String ownerTag;
InstrumentedCowArray(T[] elements, String tag) {
super(elements);
this.ownerTag = tag;
}
@Override
protected void ensureUnique() {
if (this.buffer.refCount > 1) {
long start = System.nanoTime();
super.ensureUnique();
long end = System.nanoTime();
copyCount++;
totalCopyTimeNs += (end - start);
// Log warning if copies happen frequently
if (copyCount % 100 == 0) {
System.err.println(
"WARNING: " + ownerTag + " performed " + copyCount +
" COW copies, total time: " + totalCopyTimeNs + " ns"
);
}
}
}
void printStats() {
System.out.println(
"[COW Stats: " + ownerTag + "] copies=" + copyCount +
", totalCopyTime=" + totalCopyTimeNs + " ns"
);
}
}
This instrumentation reveals hot paths where unintended mutations force expensive copies, guiding optimization efforts.
Best Practices for Interview Success
- Start with the interface, then optimize β Begin by defining clean value semantics (copy constructor, getters, setters). Add COW as an optimization afterwards, showing you understand separation of concerns.
- Draw the memory diagram β When explaining your solution, sketch boxes for buffers and arrows for references. Visual learners on the interview panel will appreciate this.
- Calculate amortized complexity β Explain that reading is O(1), copying is O(1) initially, and mutations are O(n) only when a fork occurs. The amortized cost depends on the ratio of reads to writes.
- Discuss the trade-offs β COW adds complexity and a small overhead for reference counting. It shines when reads vastly outnumber writes. If mutations are frequent, eager copying may be simpler and faster.
- Handle edge cases explicitly β Empty arrays, single-element arrays, self-assignment, and concurrent access all deserve explicit discussion. Interviewers look for this thoroughness.
- Connect to real-world examples β Mention Swift's
ArrayandDictionary, C++std::stringin older ABIs, thefork()system call, or Rust'sCowtype. This demonstrates breadth of knowledge. - Be careful with nested mutability β Always clarify whether your COW is shallow or deep. A shallow COW on an array of mutable objects still allows shared mutable state through those objects.
Conclusion
Copy-on-Write arrays represent a beautiful intersection of theory and practice. They deliver the safety of value semantics β where each instance behaves as if it owns its data independently β while cleverly avoiding the prohibitive cost of eager copying. In interviews, walking through a COW implementation demonstrates your grasp of memory management, performance optimization, and defensive programming. The pattern appears everywhere from language runtimes to operating systems, making it one of those rare topics that signals both deep understanding and practical engineering sense. Master the reference-counting fork mechanism, practice tracing buffer ownership across multiple instances, and always be ready to discuss the read-heavy workload assumption that makes COW a win. With these tools, you'll handle any COW-related interview question with confidence.