Build a Write-Ahead Log (WAL)
Build a Write-Ahead Log (WAL)
A write-ahead log is the single most important durability primitive in backend engineering: it's how databases, message queues, and replicated state machines survive a crash without losing acknowledged writes. The rule is in the name — before you mutate your in-memory state (or the main data file), you first append the change to a log and flush it to disk. If the process dies a millisecond later, recovery replays the log and nothing acknowledged is lost. Build one from an empty file and the phrase "we persist to a WAL" stops being hand-waving.
The spec
- Append(record) → durable: the call returns only once the record is safely on disk (survives
kill -9and a power cut). - Recover() → replay: on restart, return every record that was durably appended, in order.
- Torn-write safe: a crash mid-append leaves a partial record at the tail. Recovery must detect and discard it — never replay half a record as if it were whole.
- Append-only & O(1) per write: sequential writes only (that's why WALs are fast — no random I/O).
Milestones
- M1 — Framed append. Record =
[u32 length][u32 crc32(payload)][payload]. Append +fsync. The length lets recovery find record boundaries; the CRC detects corruption/torn writes. - M2 — Recovery. Read records sequentially, verify each CRC, stop at the first torn/corrupt record (the crash point). Everything before it is intact.
- M3 — Use it. Put an in-memory map behind it:
Set(k,v)appendsSET k vto the WAL then updates the map. On startup, replay the WAL to rebuild the map. You've just built a crash-safe key-value store. - M4 — Checkpoint + truncate. The WAL grows forever. Periodically snapshot the map to a file, then truncate the WAL. Recovery = load snapshot + replay the WAL tail since it.
The one idea that matters: fsync
A plain write() only copies bytes into the OS page cache — a crash loses them. fsync/fdatasync forces them to the physical device; that syscall returning is the exact moment the write becomes durable, and it's what you're really waiting on when you "acknowledge" a write. It's also the dominant cost, which is why real systems group-commit (batch many records into one fsync).
Reference — WAL core (Go)
type WAL struct{ f *os.File }
func Open(path string) (*WAL, error) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0644)
return &WAL{f}, err
}
// Append writes one framed record and fsyncs — returns only when durable.
func (w *WAL) Append(payload []byte) error {
var hdr [8]byte
binary.BigEndian.PutUint32(hdr[0:4], uint32(len(payload)))
binary.BigEndian.PutUint32(hdr[4:8], crc32.ChecksumIEEE(payload))
if _, err := w.f.Write(append(hdr[:], payload...)); err != nil {
return err
}
return w.f.Sync() // <- the durability point
}
// Recover replays intact records; a torn or corrupt tail is discarded.
func Recover(path string) ([][]byte, error) {
data, err := os.ReadFile(path)
if err != nil { return nil, err }
var recs [][]byte
for off := 0; off+8 <= len(data); {
n := binary.BigEndian.Uint32(data[off : off+4])
crc := binary.BigEndian.Uint32(data[off+4 : off+8])
end := off + 8 + int(n)
if end > len(data) { break } // torn tail: partial write
payload := data[off+8 : end]
if crc32.ChecksumIEEE(payload) != crc { break } // corruption
recs = append(recs, payload)
off = end
}
return recs, nil
}
Reference — WAL core (Java)
public final class WAL implements Closeable {
private final FileChannel ch;
public WAL(Path path) throws IOException {
ch = FileChannel.open(path, CREATE, WRITE, READ, APPEND);
}
public void append(byte[] payload) throws IOException {
CRC32 crc = new CRC32(); crc.update(payload);
ByteBuffer buf = ByteBuffer.allocate(8 + payload.length);
buf.putInt(payload.length).putInt((int) crc.getValue()).put(payload).flip();
while (buf.hasRemaining()) ch.write(buf);
ch.force(true); // fsync — durability point
}
public static List<byte[]> recover(Path path) throws IOException {
ByteBuffer b = ByteBuffer.wrap(Files.readAllBytes(path));
List<byte[]> recs = new ArrayList<>();
while (b.remaining() >= 8) {
int len = b.getInt(), crc = b.getInt();
if (b.remaining() < len) break; // torn tail
byte[] p = new byte[len]; b.get(p);
CRC32 c = new CRC32(); c.update(p);
if ((int) c.getValue() != crc) break; // corruption
recs.add(p);
}
return recs;
}
}
Tests to write (this is where it gets real)
- Round-trip: append N records, reopen,
Recoverreturns exactly those N in order. - Torn write: append 3 records, then append a valid header claiming 20 bytes but write only 2, then recover → you get exactly 3 (the tail is discarded, not a crash or a garbage 4th record). This is the test that proves your crash-safety — I validated this exact case before writing the code above.
- Durability (the honest one): in a subprocess, append + fsync, then
kill -9the process; the parent reopens and asserts the record survived. Remove theSync()/force()and this test becomes flaky — that flakiness is the lesson. - Corruption: flip a byte inside a record's payload; recovery stops there (CRC catches it) instead of replaying garbage.
Extensions — how real systems go further
- Group commit: batch many records into one
fsyncto amortize the dominant cost — 10× throughput under load. - Segment rotation: roll to a new file every N MB so old segments can be deleted after a checkpoint (Kafka logs, Postgres WAL segments).
- Replication: ship the WAL to followers — this is how leader-based replication and Raft's log work. The WAL is the shared abstraction beneath databases, queues, and consensus.
- fsync policies: per-write (safest, slowest) vs interval (
fsyncevery 10ms — Redis AOFeverysec) — the durability-vs-latency knob you'll defend in interviews.
What you've mastered when this is done
- You can explain exactly what "durable" means and pinpoint the fsync as the moment it happens — and the throughput cost that makes group-commit necessary.
- You've written the torn-write recovery that separates a real WAL from "append to a file," and tested it against a simulated crash.
- You can trace how the same log underlies database durability, queue persistence, and replication/consensus — one primitive, three systems.
🤖 Don't fully get this? Learn it with Claude
Stuck on Build a Write-Ahead Log (WAL)? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.
Build the mental picture, not memorization.
I just read a lesson on **Build a Write-Ahead Log (WAL)** (Hands-On Builds) and want to truly understand it. Explain Build a Write-Ahead Log (WAL) from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Socratic — adapts to where you're stuck.
Teach me **Build a Write-Ahead Log (WAL)** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Active recall exposes what you missed.
Quiz me on **Build a Write-Ahead Log (WAL)** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
Intuition + hook + flashcards for long-term memory.
Help me remember **Build a Write-Ahead Log (WAL)** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.