REUSABLE COMPONENT DESIGN / OBJECT DESIGN BRIEF

In-memory database design

Designing an in-memory key-value database (a mini Redis) tests data structures, command handling, and transactions.

AdvancedPhase 08 / Topic 7 of 7ResponsibilitiesCollaborationsExtensibility
01

Overview

Designing an in-memory key-value database (a mini Redis) tests data structures, command handling, and transactions. Typical requirements: SET, GET, DELETE, COUNT of keys with a given value, TTL expiry, and nested transactions with BEGIN, ROLLBACK, and COMMIT. Some variants add tables with columns, indexes, and simple queries.

Key design choices: a main hash map for data, a secondary index (value to count or set of keys) for fast COUNT queries, a stack of change logs or overlay maps for nested transactions, a min-heap or lazy checks for TTL, and a command parser mapping commands to handlers (Command pattern). Thread safety and persistence (append-only log, snapshots) are follow-ups.

Writing in pencil on draft pages

The ledger is the committed data. When you BEGIN, you write changes on a draft sheet on top. ROLLBACK tears off the draft; COMMIT copies it into the ledger. Nested drafts stack on each other.

02

When to use it

  • Interview prompt: 'Design an in-memory key-value store with transactions'.
  • Understanding Redis-like systems.
  • Machine-coding rounds focused on correctness.
03

Where it shows up in interviews

Transactional KV store

Recognize it when: SET/GET/DELETE with BEGIN/ROLLBACK/COMMIT.

  • Design an in-memory database
  • Simple Database challenge (Thumbtack)
Secondary indexes

Recognize it when: count or find keys by value quickly.

  • Design a KV store with COUNT
  • Design a table store with indexed queries
04

Where it is used in real software

Redis

In-memory data store with MULTI/EXEC transactions, TTLs, and persistence via RDB and AOF.

H2 and SQLite in-memory

Relational engines that can run entirely in memory for tests.

MVCC databases

PostgreSQL keeps row versions so transactions see snapshots, a larger-scale version of overlays.

05

Key terms

Primary store
key -> value hash map.
Secondary index
value -> count (or key set) for queries by value.
Undo log
Previous values recorded per transaction to roll back.
Nested transactions
Stack of transaction scopes; rollback affects only the innermost.
Lazy expiration
Expired keys removed when accessed, plus periodic sweeps.
06

How it works, step by step

  1. 1
    Store data

    Map<String, String> for key-value.

  2. 2
    Maintain a value-count index

    Update on every set and delete.

  3. 3
    Transactions via undo logs

    On first change of a key in a transaction, record its previous value.

  4. 4
    ROLLBACK

    Restore previous values from the top log and pop it.

  5. 5
    COMMIT

    Clear all logs (changes are already applied).

Nested transactions with undo logs
Step 1 / 4
Data: a=10
Tx1 log
Tx2 log
COUNT 10 = 1

STEP 1SET a 10 outside a transaction. Index: value 10 appears once.

07

Command session

Starting empty

Step 1 / 8
CommandOutputNotes
SET x 10-count(10)=1
BEGIN-Tx depth 1
SET x 20-log: x=10
GET x20-
COUNT 100Index updated
ROLLBACK-x restored to 10
GET x10-
ROLLBACKNO TRANSACTIONDepth 0

NOWCommand: SET x 10 | Output: - | Notes: count(10)=1

Undo logs make GET and COUNT O(1) inside transactions, and rollback costs only the number of keys changed.

08

Implementation

class InMemoryDb {  private data = new Map<string, string>();  private valueCounts = new Map<string, number>();  private txLogs: Map<string, string | undefined>[] = []; // stack of undo logs   set(key: string, value: string) { this.record(key); this.write(key, value); }  delete(key: string) { if (this.data.has(key)) { this.record(key); this.write(key, undefined); } }  get(key: string) { return this.data.get(key) ?? "NULL"; }  count(value: string) { return this.valueCounts.get(value) ?? 0; }   begin() { this.txLogs.push(new Map()); }   rollback() {    const log = this.txLogs.pop();    if (!log) return "NO TRANSACTION";    for (const [key, prev] of log) this.write(key, prev);    return "OK";  }   commit() {    if (!this.txLogs.length) return "NO TRANSACTION";    this.txLogs = [];                                     // changes already applied    return "OK";  }   private record(key: string) {    const log = this.txLogs.at(-1);    if (log && !log.has(key)) log.set(key, this.data.get(key)); // first change in this tx only  }   private write(key: string, value: string | undefined) {    const old = this.data.get(key);    if (old !== undefined) this.valueCounts.set(old, this.valueCounts.get(old)! - 1);    if (value === undefined) this.data.delete(key);    else { this.data.set(key, value); this.valueCounts.set(value, (this.valueCounts.get(value) ?? 0) + 1); }  }} // Command parser (Command pattern style dispatch)function execute(db: InMemoryDb, line: string): string {  const [cmd, a, b] = line.trim().split(/\s+/);  switch (cmd.toUpperCase()) {    case "SET": db.set(a, b); return "";    case "GET": return db.get(a);    case "DELETE": db.delete(a); return "";    case "COUNT": return String(db.count(a));    case "BEGIN": db.begin(); return "";    case "ROLLBACK": return db.rollback();    case "COMMIT": return db.commit();    default: return "UNKNOWN COMMAND";  }}
09

Complexity and performance

SET / GET / DELETE / COUNTO(1)

Hash maps.

ROLLBACKO(keys changed in tx)

Undo log size.

COMMITO(1) amortized

Discard logs.

10

Trade-offs

Undo log vs overlay maps

Undo logs make reads O(1) but writes apply immediately; overlays keep committed data untouched but reads must check each layer.

Coarse locking

synchronized methods are simple; per-key locking or single-threaded command loops (like Redis) scale better.

11

Variants and related techniques

TTL support

Store expiry times; lazy delete on access plus a heap-based sweeper.

Persistence

Append-only command log and periodic snapshots for recovery.

12

Common mistakes

  • Recording the previous value on every change in a transaction.

    Fix: Record only the first change per key per transaction.

  • Forgetting to update the COUNT index on delete or overwrite.

    Fix: Centralize writes in one method.

  • Scanning all values for COUNT.

    Fix: Maintain a value-count index.

13

Interview questions

How do you support nested transactions?

Keep a stack of undo logs. Each BEGIN pushes a log; the first change to a key within a transaction records its previous value; ROLLBACK restores from the top log and pops it; COMMIT clears all logs.

How do you make COUNT(value) O(1)?

Maintain a secondary index from value to count, updated on every set, overwrite, and delete, including during rollback.

14

Practice problems

ProblemDifficultyWhat it trains
KV store with nested transactionsMediumUndo logs.
Add TTL and snapshot persistenceHardExpiry and recovery.