← Back to DevBytes

Interview Guide: Copy-on-Write Arrays Problems and Solutions

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:

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:

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

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.

πŸš€ Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started β€” $23.99/mo
← Back to all articles