SystemDesign.website
Track 3 ยท Module 3.6 ยท DataStores & Storage Engines

NoSQL Engine Internals: LSM Trees & SSTables

How modern write-heavy databases like Apache Cassandra, RocksDB, Google Bigtable, and ScyllaDB process 500,000+ writes per second. Master the Write-Ahead Log (WAL), MemTable, immutable SSTables, Bloom Filters, and Compaction strategies.

โฑ๏ธ Reading Time: ~25 min
๐ŸŽฏ Storage Engines: RocksDB (Meta), LevelDB (Google), Cassandra
๐ŸŽฌ Format: Notebook Analogy + Step-by-Step SVGs + Live Lab

๐Ÿ’ฅ 1. Why B-Trees Hit a Wall on Heavy Writes

In Module 3.4, we learned that B-Trees are kings of read operations. But for massive write workloads (financial transaction logs, IoT sensor pings, messaging queues), B-Trees suffer from a catastrophic hardware physics bottleneck: Random In-Place Overwrites.

โŒ The B-Tree In-Place Update Penalty

When you update 1 row in a B-Tree, the engine must locate the exact 16 KB page on disk, read it, modify bytes in place, and rewrite the 16 KB block. Updating 100 random rows requires 100 random disk writes across the entire drive!

โšก The Hardware Physics Reality

On both HDDs and NVMe SSDs, Sequential Append Writes are 10x to 100x faster than Random Overwrites. Writing sequentially saturates disk bus bandwidth at peak gigabytes/sec.

๐Ÿ’ก The Core Philosophy of Log-Structured Merge-Trees (LSM Trees):NEVER overwrite existing data on disk. Every insert, update, and delete is treated as an append-only operation. We buffer writes in fast RAM, and periodically stream them to disk sequentially!

๐Ÿ›๏ธ 2. The 4 Core Pillars of an LSM-Tree Storage Engine

๐Ÿ“ The Simple Analogy: A Waiter's Notepad & The Kitchen Ledger

When you order at a restaurant, the waiter doesn't run to the back office and re-sort the company ledger alphabetically. They quickly scribble your order on a top notepad in 1 second. At the end of the shift, all notepad pages are sorted and filed away.

1Write-Ahead Log (WAL)

An append-only log file on disk. When a write arrives, it is immediately appended to the WAL so that if power fails 1 millisecond later, the data is 100% durable.

2MemTable (In-Memory SkipList)

An in-memory sorted data structure (usually a SkipList or Red-Black Tree). Writes are stored in RAM in sorted order for fast O(log N) lookups and range scans.

3SSTable (Sorted String Table)

When the MemTable reaches capacity (e.g. 64 MB), it is flushed to disk as an immutable, sorted file called an SSTable. Once written, an SSTable is never modified!

4Sparse Index & Bloom Filters

Kept in RAM to accelerate reads. The Bloom Filter instantly confirms if a key definitely does NOT exist in an SSTable, skipping useless disk seeks.

๐Ÿ”„ 3. Step-by-Step Architecture: The Write & Read Path

1 Complete LSM-Tree Architecture Diagram

Follow how writes enter the Write-Ahead Log (WAL) and MemTable in RAM, flush to Level 0 SSTables on disk, and undergo background Compaction.

Client Appput(k, v) / get(k)WriteIN-MEMORY TIER (RAM)Active MemTable (RAM)Sorted SkipList Buffer[ k10: v1, k25: v2, k40: v3 ]Bloom Filters & Sparse IndexKey Membership in RAM0.01% False Positive RatePERSISTENT DISK STORAGEWAL (Disk Log)Sequential Append OnlyFlush (64MB)SSTable_0_1.db[ k10, k25, k40 ]SSTable_0_2.db[ k15, k30, k80 ]Compaction (K-Way Merge)LEVEL 1 SSTable (Merged & De-duplicated)[ k10, k15, k25, k30, k40, k80 ]

๐Ÿชฆ 4. Deletions & Updates: The Tombstone Pattern

If SSTables on disk are 100% immutable (read-only), how do you delete or update a key without going back and editing old files?

๐Ÿชฆ The Tombstone Marker Rule:

When a user calls delete("user_42"), the database does NOT delete anything from disk! It simply inserts a special marker called a Tombstone:

set("user_42", TOMBSTONE_DELETED)
โ€ข During Reads: If the query engine encounters a tombstone as the newest record, it immediately returns 404 NOT FOUND.
โ€ข During Compaction: When the background merger combines old SSTables with the tombstone, it permanently purges both the old value and the tombstone from disk, reclaiming storage.

๐Ÿงน 5. Compaction Strategies: Size-Tiered vs Leveled Compaction

Without compaction, hundreds of flushed SSTables would pile up on disk. Reading a single key would require searching 50 different files (high Read Amplification). Databases use one of two compaction strategies:

1. Size-Tiered Compaction (STCS)

Waits until 4 SSTables of roughly equal size accumulate, then merges them together into 1 larger SSTable.

โ€ข Pros: Fast writes with minimal CPU/disk write overhead.
โ€ข Cons: High disk space overhead (needs ~50% free disk headroom during large merges).
โ€ข Best for: Write-heavy time-series and append-only event logs (Cassandra default).
2. Leveled Compaction (LCS - RocksDB)

Divides disk into levels ($L_0, L_1, L_2 \dots$). Each level is 10x larger than the previous. Keys within levels $L_1+$ are strictly non-overlapping!

โ€ข Pros: Reads touch at most 1 SSTable per level! Predictable sub-millisecond reads.
โ€ข Cons: Higher write amplification during background level cascading.
โ€ข Best for: Read-heavy / Read-Write balanced workloads (RocksDB, LevelDB).

โš”๏ธ 6. Architectural Showdown: B-Tree vs LSM-Tree

Metric / AttributeTraditional B-Tree (MySQL InnoDB, Postgres)LSM-Tree (RocksDB, Cassandra, ScyllaDB)
Write SpeedMedium (Random page overwrites)Blazing Fast (Sequential append to WAL + RAM)
Point Read SpeedUltra-Fast (1โ€“3 direct page reads)Fast with Bloom Filters (MemTable + SSTables)
Storage FragmentationHigh (Internal page holes & padding)Low (Packed immutable sequential blocks)
Write AmplificationHigh (Rewrites entire 16 KB page for 50-byte change)Medium-Low (Background compaction merges)
Ideal Production Use CaseACID relational transactions, complex multi-table joinsHigh-volume writes, time-series, messaging, analytics

๐Ÿ’ป 7. Low-Level LSM-Tree MemTable & Sparse Index Implementation

Below is the core in-memory MemTable buffering and sparse index lookup implementation:

Select Language:
// Python 3.10+ LSM-Tree Core Engine with WAL and MemTable
import os, bisect

class LSMStorageEngine:
    def __init__(self, wal_path: str = "wal.log", max_memtable_size: int = 1000):
        self.wal_path = wal_path
        self.max_size = max_memtable_size
        self.memtable = {}    # In-memory sorted buffer (SkipList)
        self.sstables = []    # List of immutable on-disk SSTables
        self.wal = open(wal_path, "a", encoding="utf-8")

    def put(self, key: str, value: str):
        # 1. Append sequentially to Write-Ahead Log on disk for durability
        self.wal.write(f"{key}:{value}\n")
        self.wal.flush()

        # 2. Insert into sorted in-memory MemTable
        self.memtable[key] = value

        # 3. If MemTable exceeds capacity, flush to immutable SSTable on disk
        if len(self.memtable) >= self.max_size:
            self.flush_memtable()

    def get(self, key: str):
        # 1. Search Active MemTable in RAM (O(1) / O(log N))
        if key in self.memtable:
            val = self.memtable[key]
            return None if val == "__TOMBSTONE__" else val

        # 2. Search SSTables from newest to oldest
        for sstable in reversed(self.sstables):
            if sstable.bloom_filter.contains(key):
                val = sstable.search(key)
                if val is not None:
                    return None if val == "__TOMBSTONE__" else val
        return None

๐Ÿงช 8. Interactive Lab: Live LSM-Tree & Compaction Simulator

โšก Real-Time LSM Storage Simulator

Interactive MemTable, SSTables & Compaction Lab

Insert keys, trigger flushes, add tombstones, and run compaction
1. Insert Key-Value into MemTable:
2. Storage Actions:
3. Background Compaction:
1. MemTable (RAM)3 / 5 Keys
2. Level 0 SSTables1 File
3. Level 1 (Compacted)1 File
๐Ÿš€ LSM Storage Active: Writes append sequentially to WAL and MemTable in RAM. When capacity reaches 5 keys, it automatically triggers a background SSTable flush!

๐ŸŽฏ 9. Test Your Understanding

Scenario 1: The Cassandra Tombstone Memory Crash

A backend service deletes 2,000,000 expired rows from an Apache Cassandra table. Right after, read queries on that table start timing out and throwing OutOfMemory (OOM) errors. Why?

Scenario 2: The Fast Ingestion Storage Strategy

You are architecting a clickstream analytics ingestion pipeline recording 400,000 events/second. Storage costs are high, but point queries are rare. Which storage engine and compaction strategy should you choose?

โ—‹

Completed this module?

Mark as finished to update your curriculum progress tracker.