Implement buffered file writer with concurrency support
DatadogSoftware Engineer · Technical ScreenLast updated
This question evaluates a candidate's ability to implement buffered I/O and design concurrency-safe APIs, exercising skills in data buffering, performance optimization, and thread-safety.
You are given a simple file writer class that writes data directly to disk:
class FileWriter {
public:
// Append `data` to the file on disk immediately (no buffering).
// May be relatively slow because it calls the OS for each write.
void write(const std::string& data);
// Flush any OS-level buffers to disk.
void flush();
};
This class is already implemented for you — you do not need to implement its real file I/O. Each call to FileWriter::write is relatively expensive because it goes to the OS, so the goal of this exercise is to reduce how often it is called.
Constraints & Assumptions
FileWriter::writeandFileWriter::flushare the only operations you may call on the underlying writer; their signatures are fixed and non-virtual.buffer_capacityis given in bytes.- All input strings passed to
writeare reasonably small compared tobuffer_capacity. - Memory allocation does not fail.
- Single-threaded for Part 1; concurrent (multiple threads) for Part 2.
Clarifying Questions to Ask
- Should the capacity-triggered flush call
flush()on the underlying writer (durability) or only forward the bytes (batching)? — i.e. what exactly does "flushed to the underlying FileWriter" mean versus "flushed to disk"? - Is the threshold inclusive — flush when the buffer is
>= buffer_capacity, or strictly>? - Am I allowed to modify the
FileWriterinterface (e.g. make its methodsvirtual), or must it stay exactly as given? This affects how I can write test doubles. - What should happen if a single
writeis larger thanbuffer_capacity, or ifbuffer_capacityis0? - For Part 2: what ordering guarantee is required across concurrent calls — full serialization, or just data integrity (no corruption / loss)?
Part 1: Implement a buffered file writer
Implement a BufferedFileWriter that uses an internal in-memory buffer to reduce the number of calls to the underlying FileWriter. Composition is preferred over inheritance (BufferedFileWriter has-a FileWriter).
class BufferedFileWriter {
public:
BufferedFileWriter(FileWriter& underlying, size_t buffer_capacity);
// Append `data` to an in-memory buffer.
// Only when certain conditions are met (e.g., the buffer is full),
// data should be flushed to the underlying FileWriter.
void write(const std::string& data);
// Force all currently buffered data to be written to the
// underlying FileWriter and then flush it to disk.
void flush();
};
Behavior:
- The constructor receives a reference to an existing
FileWriterand abuffer_capacityin bytes. write(data)appendsdatato an internal buffer; if after appending the buffer size is greater than or equal tobuffer_capacity, it writes the buffered data to the underlyingFileWriterand clears the buffer.flush()writes any remaining buffered data to the underlyingFileWriter(only if non-empty), then callsflush()on the underlyingFileWriter.
Also write (or describe) unit tests that validate:
- Writing data smaller than
buffer_capacity, then callingflush(). - Writing multiple chunks that together exceed
buffer_capacity, asserting data is forwarded at the right times. - Calling
flush()multiple times with an empty buffer is safe.
What This Part Should Cover
- Correct buffering logic:
>=threshold, clearing the buffer after forwarding, and a no-op forward when the buffer is empty. - The distinction between forwarding bytes and durably flushing (capacity trigger forwards but does not fsync;
flush()does both). - Composition over inheritance, with the underlying writer held by reference.
- Tests that assert timing/behavior (data withheld until flush; capacity trigger does not fsync), not just final concatenated bytes — which forces a deliberate, working test-double strategy.
Part 2: Make BufferedFileWriter thread-safe
Now assume BufferedFileWriter may be used from multiple threads concurrently — different threads may call write() and flush() at the same time. Extend your design so it is thread-safe, and provide pseudocode (any language) showing how shared state is protected and how write() and flush() coordinate.
Clarifying Questions for this Part
- What durability scope must
flush()guarantee for writes that are racing with it (started but not yet returned) versus writes that completed beforeflush()was called? - Is byte-on-disk order required to match the order in which
writecalls returned, or only that eachwrite's bytes land contiguously? - Is a dedicated background-flush thread or a max-buffer backpressure limit in scope (these would justify a condition variable), or is a synchronous design sufficient?
What This Part Should Cover
- A precisely stated thread-safety guarantee (what is and is not promised) before any code.
- A single mutex guarding all access to the buffer, with the critical section covering the full append-and-conditionally-forward sequence.
- An argument for why this is race-free (no torn buffer, no lost/duplicated bytes, each forwarded chunk is exactly one thread's accumulated buffer).
- Awareness of the I/O-under-lock cost and the buffer-swap optimization with its ordering caveat; recognizing a plain mutex suffices and a condition variable is only needed for waiting.
Hints
Hint 1
Where to start: This is the classic **batching** pattern (think stdio `setvbuf` / Rust `BufWriter`): accumulate small writes in a `std::string` member and forward to the underlying writer only when a threshold is crossed.
Hint 2
The flush contract — keep two things separate: There are *two* distinct actions: (a) forwarding buffered bytes to `underlying_.write(...)`, and (b) calling `underlying_.flush()` for durability. The capacity trigger in `write` should do **only (a)** — forwarding without an fsync — otherwise you defeat the purpose of buffering. `flush()` does both (a) then (b).
Hint 3
Testing against a non-virtual interface: Because `FileWriter::write/flush` are **non-virtual** and you hold a `FileWriter&`, a derived "mock : public FileWriter" override is dispatched statically and never reached. To get an inspectable recorder *without* changing the given interface, template the class on the writer type and instantiate it with a duck-typed recording double in tests.
Hint 4
What to protect: The buffer (and the conditional "forward then clear" sequence) is the shared mutable state. The whole append-and-maybe-forward is a multi-step read-modify-write, so a single `std::mutex` held across the entire critical section is the simplest correct mechanism — atomics alone cannot make a multi-step string operation indivisible.
Hint 5
Pin down the guarantee before coding: State the contract: each `write`/`flush` takes effect atomically in *some* serial order; one `write(data)`'s bytes are forwarded contiguously (never spliced with another thread's), and nothing is lost or duplicated. Decide explicitly whether you also promise wall-clock ordering across concurrent calls (you usually shouldn't).
Hint 6
Senior refinement (and its trap): Holding the lock across the slow underlying `write` serializes all I/O. You can shrink the critical section by *swapping the full buffer out under the lock* and doing the syscall outside it — but call out the hazard: two swapped chunks can then race in `underlying.write`, reordering bytes on disk. The fix is a second I/O-ordering lock, or just accept serialized I/O.
Follow-up questions
- How would you add a dedicated background flush thread (or a time-based flush) so callers never block on the syscall? What synchronization primitive does that require, and how do you shut it down cleanly?
- Suppose a single `write` is *larger* than `buffer_capacity`. What does your Part 1 code do, and how would you special-case an oversized chunk to bypass the buffer?
- If the process can crash, what data can be lost, and how do `flush()` semantics (and the OS `flush`) bound that window? How would you offer an `O_DIRECT`/fsync-per-write durability mode?
- In Part 2, how would you add **backpressure** so a fast producer can't grow memory unbounded while the underlying writer is slow? Where does a condition variable enter, and what deadlock must you avoid?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.