System design sample solution

System design: build a multi-threaded log writer

A step-by-step system design answer you could actually give in 60 minutes.

System design interview · Sample 60-minute answer · Not Databricks's actual design · Independent, not affiliated with Databricks

The system design question

Build a logging system that lots of threads in one program can write to. Every entry has to end up in the file complete and in the order it was written, and it needs to keep up as you add threads. This one's a single-machine design.

Agree on the scope (5 minutes)

Say up front what you're covering.

We'll designWe'll skip
Many threads writing at onceSending logs to other machines
Keeping entries whole and in orderSearching logs
Getting entries onto diskLog formats
Starting a new file when one gets bigDeleting old logs

Then agree on what "good" means:

  • Correct: entries never get mixed together, and they stay in the order they were written.
  • Fast for writers: logging shouldn't slow down the threads doing real work.
  • Safe: agree with your interviewer how much can be lost if the program crashes.
  • Scales: adding threads shouldn't make things fall apart.

Rough numbers (5 minutes)

Made up, but reasonable: 32 threads writing 100,000 entries a second in total, about 200 bytes each.

100K
entries per second
~20 MB
written per second
~200 MB/s
a disk can write in order
~1K
times a second you can force data onto disk
QuestionRough mathWhat it means
Can the disk keep up?20 MB/s against about 200 MB/sYes, if we write in big, ordered chunks
Can we force every entry to disk?100K entries vs about 1K forced writes a secondNo, we have to group entries together
How much is in memory at once?Say 50 ms of entriesAbout 1 MB, which is nothing

The overall design (10 minutes)

Writer threadscall log()In-memory queuekeeps the orderSingle writerdrains in batchesBuffera big chunkLog fileon disk
Threads just drop entries on a queue. One writer thread owns the file.
  1. A thread calls log() with its entry.
  2. The entry goes on an in-memory queue. Its place in the queue is its place in the file.
  3. One background writer thread takes entries off the queue in batches.
  4. It adds each batch to a buffer and writes the buffer to the file in one go.
  5. Every few milliseconds, it forces what's written onto the disk.

What the apps need to do

ActionWho does itWhat's sent
Log an entryAny threadThe message, which gets a timestamp and sequence number
FlushThe programWaits until everything logged so far is on disk
CloseThe programWrites everything left, then stops the writer

What we store and where

DataWhere it goesWhy
Waiting entriesAn in-memory queue with a size limitHands work from many threads to one
The current chunkA buffer in memoryOne big write is much cheaper than lots of small ones
Log filesDiskStart a new file when the current one gets big

Step 1: Start simple: one lock (5 minutes)

The simplest correct version wraps every write in a lock: only one thread writes at a time, so entries never get mixed. Say this first. It's correct, and it gives you something to improve.

The problem is that every thread waits on the lock and on the disk. As you add threads, they spend more and more time just waiting.

Step 2: One writer and a queue (15 minutes)

Instead of every thread writing to the file, threads put their entry on a queue and go back to work. One writer thread is the only thing that ever touches the file. That's the same idea as a message queue, just inside one program.

What does 'in order' mean with lots of threads? The cleanest answer is the order entries were added to the queue. Give each entry a sequence number as it goes in, and order in the file always matches.

Adding to the queue still needs a little locking, but it only lasts a moment, so threads barely wait.

When the queue fills up

If threads log faster than the disk can write, the queue grows. Give it a size limit and pick a rule: make threads wait (nothing lost, but slower), or drop low-priority entries (fast, but you lose some). Usually, wait for errors and drop debug logs.

Step 3: Batching and getting onto disk (10 minutes)

The writer takes everything waiting on the queue at once, writes it as one chunk, and repeats. Under heavy load, batches get bigger and it gets more efficient.

Writing to a file doesn't mean it's on disk yet. The operating system holds it in memory for a while. Forcing it to disk is slow, so do it every 10 milliseconds or so, not for every entry. That's a trade-off: if the program crashes, you lose up to the last 10 milliseconds. Check with your interviewer what's acceptable, and offer a 'wait until this is safe' option for important entries.

When a file reaches a size limit, the writer closes it and starts a new one. Only the writer touches files, so no other thread has to know.

Choices you should be able to defend

We choseInstead ofBecauseWe'd reconsider if
One writer thread and a queueA lock around the fileThreads barely wait, and order is simpleThere were only ever a couple of threads
Forcing to disk every few millisecondsForcing every entry to diskIt's about 100 times fasterLosing any entry in a crash was unacceptable
Making threads wait when the queue's fullDropping entriesNothing important gets lostLogging must never slow the program down
One shared fileA file per thread, merged laterOrder is already correctEven the queue became a bottleneck

What could go wrong (5 minutes)

What happensWhat we do about it
The program crashesUp to the last few milliseconds of entries can be lost, and important ones can wait to be safe
The disk fills upStop writing debug logs, raise an alert, and keep the most important entries
The disk is slow for a momentThe queue absorbs it. If it fills, the full-queue rule kicks in
The writer thread diesRestart it, and have log() report an error instead of hanging forever
Someone logs a giant entryCap the size of one entry so it can't block everyone else

Follow-up questions (5 minutes)

How would you send logs to a central place?

Have the writer also hand batches to a shipper that sends them over the network, with its own queue so a slow network doesn't slow the file writer.

Could you avoid the queue lock entirely?

Give each thread its own small buffer and number entries with one shared counter, then have the writer merge buffers in number order.

What about multiple processes?

Each process writes its own file, and a separate tool merges them by timestamp when needed.

How the 60 minutes add up

MinutesWhat you're doing
0–5Agree on what you're building
5–10Rough numbers
10–20Draw the overall design
20–25One lock
25–40One writer and a queue
40–50Batching and getting onto disk
50–55What could go wrong
55–60Follow-up questions

Key concepts used here

Further reading

We wrote this sample answer ourselves.

Practice Loop is independent and isn't affiliated with or endorsed by Databricks.