PracHub
QuestionsCoachesLearningGuidesInterview Prep

Quick Overview

This question evaluates skills in designing and implementing in-memory data structures, TTL-based expiration, lexicographic prefix scans, and backup/restore snapshot semantics within the Coding & Algorithms domain.

  • medium
  • Coinbase
  • Coding & Algorithms
  • Software Engineer

Design an in-memory database with TTL and backups

Company: Coinbase

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: medium

Interview Round: Take-home Project

You are asked to implement a small in-memory “database” that evolves across 4 parts. In each part you may reuse your previous code and only add/extend methods. ### Data model Each record is identified by a **string key** and stores a **string value**. Time is represented by an integer timestamp `t` (monotonically increasing in tests). --- ## Part 1 — Basic read/write Implement a database supporting: - `put(key, value, t)`: store `value` for `key`. - `get(key, t) -> value | null`: return the currently stored value for `key`, or `null` if missing. Notes: - If `put` is called multiple times for the same key, the latest write should be returned. --- ## Part 2 — Scan Add: - `scan(prefix, t) -> List<(key, value)>`: return all key/value pairs whose **key starts with `prefix`**. Requirements: - Results must be sorted by key in lexicographic order. --- ## Part 3 — Expiration (TTL) Extend `put` to optionally accept a TTL: - `put(key, value, t, ttlSeconds)` means the entry is valid for timestamps in the half-open interval `[t, t + ttlSeconds)`. After that it is expired. Update `get` and `scan` so that expired items are not returned. Clarifications: - If a key is overwritten with a new `put`, the new value/TTL replaces the old one. - Keys without TTL never expire. --- ## Part 4 — Backup / Restore Add support for backups: - `backup(t) -> backupId`: captures the database state at time `t` (only non-expired entries at time `t`). - `restore(backupId, t)`: restores the database to exactly the state captured in that backup. TTL behavior on restore: - If an entry had remaining TTL at backup time, it should still expire after the remaining time elapses following the restore (i.e., expiration is based on the original timestamps/remaining lifetime, not “reset” to a fresh TTL). --- ### What you need to deliver Implement the required methods so that all provided test cases pass. ### Constraints (typical for this style of OA) - Up to ~10^5 operations. - Keys/values are short strings. - Aim for efficient lookups and scans.

Quick Answer: This question evaluates skills in designing and implementing in-memory data structures, TTL-based expiration, lexicographic prefix scans, and backup/restore snapshot semantics within the Coding & Algorithms domain.

Part 1: Basic read/write

Implement a simple **in-memory key-value store** that processes a sequence of operations and returns the result of every read. ## Function ``` solution(operations) ``` `operations` is a list of operations. Apply them **in the given order** and return a list containing one entry for each `get` operation, in the order those reads occur. ## Input format Each operation is itself a list whose first element is the command name: - **Put** — `['put', key, value, t]` Store the string `value` under the string `key`. If `key` already has a value, overwrite it so the key always holds its most recent write. - **Get** — `['get', key, t]` Read the value currently stored for `key`. Here `key` and `value` are short strings, and `t` is an integer timestamp. ## Output Return a list with **one element per `get` operation**, in read order: - the latest value stored for that `key`, or - `None` if the key has never been written. `put` operations produce no output. ## Timestamps Every operation carries a timestamp `t`, and timestamps are **nondecreasing** across the input. In this part, `t` does **not** affect behavior — a `get` always returns the most recent write to its key. (It is included for consistency with later parts.) ## Constraints - `0 <= len(operations) <= 10^5` - Keys and values are short strings. - Timestamps are integers and are nondecreasing across operations. - Only the latest write for a key matters. ## Examples - `[['put', 'a', 'x', 1], ['get', 'a', 2], ['put', 'a', 'y', 3], ['get', 'a', 4], ['get', 'b', 5]]` → `['x', 'y', None]` (`a` reads `x`, then is overwritten to `y`; `b` was never written, so it reads `None`.) - `[['get', 'missing', 1]]` → `[None]` - `[]` → `[]`

Constraints

  • 0 <= len(operations) <= 10^5
  • Keys and values are short strings
  • Timestamps are integers and are nondecreasing across operations
  • Only the latest write for a key matters

Examples

Input: [['put', 'a', 'x', 1], ['get', 'a', 2], ['put', 'a', 'y', 3], ['get', 'a', 4], ['get', 'b', 5]]

Expected Output: ['x', 'y', None]

Explanation: The second put overwrites key 'a'. Key 'b' was never written.

Input: [['get', 'missing', 1]]

Expected Output: [None]

Explanation: Edge case: reading a missing key returns None.

Input: []

Expected Output: []

Explanation: Edge case: no operations means no outputs.

Input: [['put', 'k1', 'v1', 1], ['put', 'k2', 'v2', 2], ['get', 'k1', 3], ['get', 'k2', 4]]

Expected Output: ['v1', 'v2']

Explanation: Multiple keys are stored independently.

Hints

  1. A hash map from key to value is enough for this part.
  2. When the same key is written again, simply replace the old value.

Part 2: Scan by prefix

Implement an **in-memory key/value database** that supports lookups and **prefix scans**, extending the basic store with a `scan` operation. ### Function ``` solution(operations) ``` You are given a list `operations`, where each element is an array describing one operation. Process the operations **in order** and return a list containing the results of the operations that produce output (described below). ### Operations Each operation is an array whose first element is the command name: - **`['put', key, value, t]`** — Store `value` under `key`. If `key` already exists, **overwrite** its value with `value`. This operation produces **no output**. - **`['get', key, t]`** — Append the **current value** stored under `key` to the result. If `key` is not present, append `None` (null). - **`['scan', prefix, t]`** — Append a list of all currently stored entries whose **key starts with `prefix`**. Each matching entry is represented as the pair `[key, value]`, and the list is **sorted lexicographically by key**. If no key matches, append an **empty list** `[]`. Here `key`, `value`, and `prefix` are short strings, and `t` is an integer timestamp. ### Output Return a list whose entries are, in order, the results of every `get` and `scan` operation (a `put` contributes nothing): - A `get` contributes a single value (the stored string, or `None` if the key is absent). - A `scan` contributes a list of `[key, value]` pairs, sorted lexicographically by key. ### Rules & notes - Only the **most recent** `put` to a key is in effect; later puts overwrite earlier ones. - For `scan`, a key matches when it **starts with** `prefix`. An **empty prefix `''` matches every stored key**, so the scan returns all entries sorted by key. - **Timestamps** `t` are integers and are **nondecreasing** across the input, but they **do not affect behavior** in this part — you can treat each `put` as simply setting the current value. ### Constraints - `0 <= len(operations) <= 10^5` - Keys, values, and prefixes are short strings. - Timestamps are integers and are nondecreasing across operations. - Scan results must be sorted lexicographically by key. ### Example For the operations: ``` [['put', 'car', 'red', 1], ['put', 'cat', 'blue', 2], ['put', 'car', 'green', 3], ['get', 'car', 4], ['scan', 'ca', 5]] ``` the result is: ``` ['green', [['car', 'green'], ['cat', 'blue']]] ``` The two puts to `car` leave it set to `green`, so `get('car')` returns `'green'`, and `scan('ca')` returns both keys sorted lexicographically.

Constraints

  • 0 <= len(operations) <= 10^5
  • Keys, values, and prefixes are short strings
  • Timestamps are integers and are nondecreasing across operations
  • Scan results must be sorted lexicographically by key

Examples

Input: [['put', 'apple', '1', 1], ['put', 'app', '2', 2], ['put', 'banana', '3', 3], ['scan', 'app', 4]]

Expected Output: [[['app', '2'], ['apple', '1']]]

Explanation: Only keys starting with 'app' are returned, sorted lexicographically.

Input: [['put', 'car', 'red', 1], ['put', 'cat', 'blue', 2], ['put', 'car', 'green', 3], ['get', 'car', 4], ['scan', 'ca', 5]]

Expected Output: ['green', [['car', 'green'], ['cat', 'blue']]]

Explanation: Overwriting a key changes both get and scan results.

Input: [['put', 'b', '2', 1], ['put', 'a', '1', 2], ['scan', '', 3]]

Expected Output: [[['a', '1'], ['b', '2']]]

Explanation: Edge case: an empty prefix matches all keys.

Input: [['put', 'dog', '1', 1], ['scan', 'z', 2], ['get', 'cat', 3]]

Expected Output: [[], None]

Explanation: Edge case: no keys match the prefix, and a missing key returns None.

Hints

  1. Use one structure for fast key -> value lookup and another to keep keys ordered.
  2. For scan, binary search can help you jump to the first possible matching key.

Part 3: Expiration with TTL

Implement an in-memory key/value store that supports **time-to-live (TTL) expiration**, then replay a chronological log of operations against it. You are given a list `operations`. Each element is itself a list whose first element is the command name (`'put'`, `'get'`, or `'scan'`), followed by that command's arguments. Process the operations **in the given order** (timestamps are nondecreasing). Return a list containing one result for **each `get` and `scan` operation**, in the order those operations appear. `put` operations produce no output. ## Operations - **`['put', key, value, t]`** — Store `value` under `key` at time `t` with **no expiration** (the value is valid for all times `>= t` until overwritten). - **`['put', key, value, t, ttlSeconds]`** — Store `value` under `key` at time `t` with a TTL. The value is valid only during the **half-open interval `[t, t + ttlSeconds)`** — that is, for every time `T` with `t <= T < t + ttlSeconds`. - **`['get', key, t]`** — Append the current value of `key` at time `t` to the output. If `key` was never written, or its latest entry has expired by time `t`, append `None`. - **`['scan', prefix, t]`** — Append a list of `[key, value]` pairs to the output: one pair for every key that **starts with `prefix`** and whose latest entry is still valid at time `t`. The pairs must be **sorted lexicographically by key**. ## Rules - **Overwrite semantics:** Writing to an existing `key` replaces its previous value *and* its previous TTL. Only the most recently written entry for a key is ever considered. - **Expiration:** An entry written at time `t` with `ttlSeconds` is expired at any query time `T >= t + ttlSeconds`. Expired entries must never be returned by `get` or `scan`. - In particular, a TTL of `0` means the entry is **already expired** at the instant it is written (since `t < t + 0` is never true). - `get` returns the scalar value (or `None`); `scan` returns a list of `[key, value]` pairs. ## Function signature ```python def solution(operations): pass ``` ## Constraints - `0 <= len(operations) <= 10^5` - Keys, values, and prefixes are short strings. - Timestamps are integers and are nondecreasing across operations. - `0 <= ttlSeconds <= 10^9` - An entry with TTL is valid exactly for times `t` such that `start_time <= t < start_time + ttlSeconds`. ## Examples **Example 1** ``` operations = [['put', 'a', 'x', 1, 5], ['get', 'a', 1], ['get', 'a', 5], ['get', 'a', 6]] output = ['x', 'x', None] ``` `'a'` is valid on `[1, 6)`: it is present at times 1 and 5, but expired at time 6. **Example 2** ``` operations = [['put', 'k', 'v1', 1, 2], ['get', 'k', 2], ['put', 'k', 'v2', 4], ['get', 'k', 5]] output = ['v1', 'v2'] ``` At time 2, `'k'` still holds `'v1'` (valid on `[1, 3)`). The second `put` overwrites it with `'v2'` (no TTL), so the later `get` returns `'v2'`. **Example 3** ``` operations = [['put', 'app', '1', 1, 3], ['put', 'apple', '2', 2], ['put', 'ape', '3', 3, 1], ['scan', 'ap', 3], ['scan', 'ap', 4]] output = [[['ape', '3'], ['app', '1'], ['apple', '2']], [['apple', '2']]] ``` At time 3, all three keys are valid, returned sorted by key. At time 4, `'app'` (valid on `[1, 4)`) and `'ape'` (valid on `[3, 4)`) have expired, leaving only `'apple'`. **Example 4** ``` operations = [['put', 'z', 'gone', 10, 0], ['get', 'z', 10], ['scan', 'z', 10]] output = [None, []] ``` A TTL of `0` expires immediately, so `'z'` is never visible to `get` or `scan`.

Constraints

  • 0 <= len(operations) <= 10^5
  • Keys, values, and prefixes are short strings
  • Timestamps are integers and are nondecreasing across operations
  • 0 <= ttlSeconds <= 10^9
  • An entry with TTL is valid exactly for times t such that start_time <= t < start_time + ttlSeconds

Examples

Input: [['put', 'a', 'x', 1, 5], ['get', 'a', 1], ['get', 'a', 5], ['get', 'a', 6]]

Expected Output: ['x', 'x', None]

Explanation: The key is valid for times 1 through 5, and expires at time 6.

Input: [['put', 'k', 'v1', 1, 2], ['get', 'k', 2], ['put', 'k', 'v2', 4], ['get', 'k', 5]]

Expected Output: ['v1', 'v2']

Explanation: A later put replaces the old value and expiration.

Input: [['put', 'app', '1', 1, 3], ['put', 'apple', '2', 2], ['put', 'ape', '3', 3, 1], ['scan', 'ap', 3], ['scan', 'ap', 4]]

Expected Output: [[['ape', '3'], ['app', '1'], ['apple', '2']], [['apple', '2']]]

Explanation: At time 3 all three keys are visible. At time 4 only the non-expiring key remains.

Input: [['put', 'z', 'gone', 10, 0], ['get', 'z', 10], ['scan', 'z', 10]]

Expected Output: [None, []]

Explanation: Edge case: TTL 0 means the valid interval is [10, 10), so the key is never visible.

Hints

  1. Instead of storing TTL directly, store an absolute expiration time.
  2. Be careful with the half-open interval: when current time equals expire time, the item is already expired.

Part 4: Backup and restore with TTL preservation

Implement an in-memory key-value database that supports time-based expiry (TTL), plus point-in-time **backup** and **restore** where a restored entry keeps its *remaining* TTL rather than getting a fresh one. ## Function ```python def solution(operations): ... ``` `operations` is a list of operations to process **in the given order**. Each operation is itself a list whose first element is the operation name. Every timestamp `t` is an integer, and timestamps are **nondecreasing** across operations. Return a list containing the result of each operation **that produces output**, in the order those operations occur. ## Operations Each operation has one of the following shapes: - **`['put', key, value, t]`** — Store `value` under `key` with **no expiry** (it never expires). If `key` already exists, overwrite it. **Produces no output.** - **`['put', key, value, t, ttlSeconds]`** — Store `value` under `key` with a time-to-live of `ttlSeconds`. The entry is considered alive for any time strictly **before** `t + ttlSeconds`, and is expired at or after that time. Overwrites an existing key. **Produces no output.** - **`['get', key, t]`** — Return the stored value for `key` if it exists and is **not expired** at time `t`; otherwise return `None`. **Appends the value (or `None`) to the output.** - **`['scan', prefix, t]`** — Return a list of `[key, value]` pairs for every key that starts with `prefix` and is **not expired** at time `t`, sorted **lexicographically by key**. An empty `prefix` matches all keys. **Appends this list (possibly empty) to the output.** - **`['backup', t]`** — Snapshot the database **as of time `t`**, capturing only entries that are **not expired** at `t`. For each captured entry that has a TTL, store its **remaining** time (`expire_time - t`) rather than an absolute expiry. Assign and return a new integer backup id; ids start at **1** and increase by 1 with each backup. **Appends the backup id to the output.** - **`['restore', backupId, t]`** — Replace the **entire** current database with the state saved in backup `backupId`. For each restored entry that had remaining TTL, it expires that many seconds **after** the restore — i.e. at `t + remaining` — so an entry with, say, 5 seconds of TTL left at backup time will expire 5 seconds after this restore, not on a fresh full TTL. Non-expiring entries stay non-expiring. **Produces no output.** `backupId` is always a valid, previously returned id. ## Expiry rule An entry created (or restored) so that it expires at time `E` is **alive while `t < E`** and **expired once `t >= E`**. Non-expiring entries (`put` without `ttlSeconds`) are always alive until overwritten or replaced by a restore. ## Output The returned list contains, in order, one element per `get` / `scan` / `backup` operation: - `get` → the value or `None` - `scan` → a list of `[key, value]` pairs sorted by key (may be empty) - `backup` → the integer backup id ## Constraints - `0 <= len(operations) <= 10^5` - Keys, values, and prefixes are short strings. - Timestamps are integers and are nondecreasing across operations. - `0 <= ttlSeconds <= 10^9` - `backupId` passed to `restore` is always valid. - Only non-expired entries at backup time are stored in that backup. ## Example ``` operations = [ ['put', 'a', '1', 1, 2], # 'a' expires at t = 3 ['put', 'b', '2', 2], # 'b' never expires ['backup', 4], # at t=4 'a' is already expired -> only 'b' captured; returns 1 ['restore', 1, 10], # restore backup 1; 'b' restored, 'a' is gone ['scan', '', 11] # scan all live keys ] # Output: [1, [['b', '2']]] ```

Constraints

  • 0 <= len(operations) <= 10^5
  • Keys, values, and prefixes are short strings
  • Timestamps are integers and are nondecreasing across operations
  • 0 <= ttlSeconds <= 10^9
  • backupId used in restore is always valid
  • Only non-expired entries at backup time are stored in that backup

Examples

Input: [['put', 'a', 'x', 1], ['backup', 2], ['put', 'a', 'y', 3], ['get', 'a', 4], ['restore', 1, 5], ['get', 'a', 6]]

Expected Output: [1, 'y', 'x']

Explanation: After restore, the database returns to the exact state saved in backup 1.

Input: [['put', 'temp', 'v', 1, 10], ['backup', 5], ['restore', 1, 20], ['get', 'temp', 25], ['get', 'temp', 26]]

Expected Output: [1, 'v', None]

Explanation: At backup time 5, the key has 6 seconds remaining. After restore at 20, it expires at 26.

Input: [['put', 'a', '1', 1, 2], ['put', 'b', '2', 2], ['backup', 4], ['restore', 1, 10], ['scan', '', 11]]

Expected Output: [1, [['b', '2']]]

Explanation: Key 'a' is already expired at backup time, so it is not saved.

Input: [['backup', 1], ['put', 'x', '1', 2], ['restore', 1, 3], ['get', 'x', 4], ['scan', '', 4]]

Expected Output: [1, None, []]

Explanation: Edge case: restoring an empty backup clears the database.

Hints

  1. At backup time, save only live entries. For expiring entries, save the remaining lifetime instead of the original expiration time.
  2. On restore at time t, rebuild each saved expiration time as t + remaining_lifetime.
Last updated: Apr 20, 2026

Loading coding console...

PracHub

Master your tech interviews with 8,500+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • AI Coding Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • Implement an In-Memory Database - Coinbase (hard)
  • Implement a Coin-Constrained Jump Strategy - Coinbase (hard)
  • Implement Game Physics and Block Mining - Coinbase (hard)
  • Compute Total Manual Distance - Coinbase (medium)
  • Implement a Flappy Bird Jump Agent - Coinbase (medium)