Skip to main content

Command Palette

Search for a command to run...

Building an LSM Tree Storage Engine from Scratch in C++

Updated
•5 min read•View as Markdown
Building an LSM Tree Storage Engine from Scratch in C++
P
Developer. Systems Engineer.

Most developers use databases.

Very few understand how they work internally.

To bridge that gap, I built a Log-Structured Merge Tree (LSM Tree) based key-value storage engine from scratch in C++ to understand how modern storage engines optimize disk I/O, durability, and performance.

What This Project Is

This project is a key-value storage engine based on the LSM Tree design.

The core idea is simple:

Convert random writes into sequential writes.

Traditional structures like B-Trees perform in-place updates, which result in random disk I/O. LSM Trees avoid this by appending data and organizing it efficiently in the background.

Why This Project Matters

Even if this implementation does not match production systems like LevelDB or RocksDB, it demonstrates an understanding of:

  • Disk I/O behavior

  • Data durability

  • Storage engine design

  • Performance tradeoffs

  • Internal database architecture

Most engineers use databases as black boxes. This project focuses on understanding what happens inside them.

High-Level Architecture

Write Path

PUT → Write-Ahead Log → Memtable → Immutable Memtable → SSTable → Compaction

Read Path

GET → Memtable → Bloom Filter → Block Index → Disk Block

The system is designed to optimize writes while still keeping reads efficient.

Core Components and Design Decisions

Write-Ahead Log (WAL)

The WAL is an append-only log that records all operations.

Why it exists:

  • Ensures durability

  • Enables crash recovery

Append-only writes are used because sequential disk writes are significantly faster than random writes.

On restart, the system replays the WAL to rebuild the memtable. If a partially written entry is detected, replay stops to avoid corruption.

Memtable

The memtable is an in-memory sorted structure implemented using std::map.

Why sorted order matters:

  • Efficient SSTable generation

  • Maintains ordered disk layout

std::map was chosen over unordered_map because it maintains sorted order with logarithmic insert time.

Immutable Memtable

Flushing the memtable to disk can block writes.

To avoid this:

  • The active memtable is converted into an immutable memtable

  • A new memtable is created for incoming writes

  • Flushing happens in the background

This decouples write performance from disk I/O.

SSTables

SSTables are immutable, sorted files stored on disk.

Why immutable:

  • No in-place updates

  • Simplifies concurrency

  • Enables efficient sequential writes

Binary Storage Format

Instead of storing data as text, the system uses a binary format:

[key_size][key][value_size][value]

Why:

  • Faster reads

  • Exact offsets

  • No parsing overhead

  • More compact storage

Block-Based Storage

Disk reads happen in pages (typically around 4KB).

Instead of reading individual records, the system reads blocks containing multiple records.

Structure:

[data blocks][block index][footer]

This improves read efficiency significantly.

Block Index

The block index maps a key range to a disk offset.

This allows:

  • Jumping directly to relevant data

  • Avoiding full file scans

Bloom Filters

Bloom filters are used to quickly check whether a key is definitely not present.

This prevents unnecessary disk reads, especially for negative lookups.

Bloom filters allow false positives but never false negatives.

Compaction

Over time, multiple SSTables accumulate with:

  • Duplicate keys

  • Outdated values

  • Deleted entries

Compaction merges SSTables, keeps the latest values, and removes unnecessary data.

This process runs in the background to avoid blocking writes.

Background Processing

A worker thread handles:

  • Flushing memtables

  • Compaction

This ensures writes remain fast and non-blocking.

The system uses std::thread with condition_variable to avoid inefficient polling.

Concurrency Design

The system uses:

  • mutex

  • condition_variable

  • task queue

This design separates producers (writes) from consumers (background tasks), allowing asynchronous processing.

Tradeoffs

To keep the implementation focused, several features were simplified:

  • No multi-level compaction

  • No block cache

  • No compression

  • Single background worker thread

These decisions prioritize clarity of design over full production complexity.

Running the System

Compile:

g++ -std=c++17 main.cpp storage_engine.cpp memtable.cpp wal.cpp sstable.cpp compaction.cpp bloom_filter.cpp -o lsm_store

Run:

./lsm_store

Testing

Basic operations:

  • PUT key value

  • GET key

  • DELETE key

Crash recovery test:

  • Insert data

  • Kill the program

  • Restart and verify data

Load testing can be done by inserting large numbers of keys and observing SSTable creation and compaction behavior.

Benchmarking

Key metrics:

  • Write throughput

  • Read latency

  • Compaction overhead

Expected behavior:

  • Writes are fast due to in-memory buffering and sequential logging

  • Reads are slower due to disk lookups

Key Concepts (Interview Focus)

Why LSM instead of B-Tree?

LSM Trees optimize for write-heavy workloads using sequential I/O, while B-Trees rely on random writes.

Why Bloom Filters?

They reduce unnecessary disk access by quickly ruling out missing keys.

What is write amplification?

Data is rewritten multiple times during compaction.

What happens on crash?

The WAL is replayed to restore the system state.

What I Would Improve

  • Multi-level compaction

  • Block caching

  • Compression

  • Parallel compaction

Conclusion

Building an LSM Tree from scratch provides a deep understanding of how modern storage engines work.

The key insight is that performance comes from designing around hardware constraints, especially disk behavior.

By converting random writes into sequential operations and using background processes, the system achieves high write throughput while maintaining acceptable read performance.