In this article
  1. Before you start
  2. Why interviewers ask this
  3. The in-memory database task from first principles
  4. Worked example
  5. Trade-offs and edge cases
  6. How to answer it in an interview
  7. Common mistakes
  8. Follow-up questions
  9. Sources

Before you start

This page belongs to the industry coding guide, which covers how machine coding rounds work.

Why interviewers ask this

The task tests whether you can grow a program without breaking it, which is most of real engineering. Each level adds a requirement on top of the code you already wrote, and CodeSignal's technical brief asks for "retaining the integrity of previously implemented method contracts" and says "do not change the existing method signatures".3 A candidate who writes level 1 as a throwaway then pays for it at level 3.

A strong answer picks a storage shape that survives later requirements, keeps the old methods working, and says out loud what each new level will cost in time. Companies use the Industry Coding Framework to judge mid-level and senior engineers, and CodeSignal says its levels assess whether you can reuse earlier code and add features without regressions.1 The same habit shows up in machine learning coding rounds, in the loops described in interview loops and in the behavioral interviews guide.

The in-memory database task from first principles

In plain words

In-memory database
An in-memory database is a data store that keeps its records in the program's memory, not on disk, so access is fast and the data vanishes when the program ends.
Timestamp
A timestamp is a number saying when an operation happens, here an integer clock the caller passes in.

Precisely: a value set at time t with time to live k is readable while the timestamp is below t + k.

Picture a filing cabinet. Each drawer has a label, the key. Inside are cards, each with a field name and a value. Setting a card, reading it and removing it is level 1. Asking for all the cards in one drawer, in order, is level 2. Each later level adds a rule to the same cabinet, and the earlier rules must keep working. The analogy breaks at level 3: a real card never vanishes alone, but a card with a time to live does, and the cabinet must act as if it were gone once the clock passes its time.

Step 1: Level 1 stores, reads and deletes fields

Level 1 is a dictionary of dictionaries: key to field to value. set writes a value, get returns it or nothing, and delete returns true if something was removed and false if nothing was there. CodeSignal says level 1 assesses basic implementation, simple data structures and covering corner cases.1

The corner cases are the points. Reading a missing key must not raise an error. Deleting the last field of a key should remove the key, so an empty record does not hold memory or show up when keys are listed. Deleting something twice must return false the second time. The code stays small:

def set(self, key, field, value):
    self._d.setdefault(key, {})[field] = (value, None)
def delete(self, key, field):
    rec = self._d.get(key)
    if rec is None or field not in rec: return False
    del rec[field]
    if not rec: del self._d[key]
    return True

Notice that each value is stored as a pair of the value and an expiry that is empty for now. That one decision is the whole reason Step 3 is easy, and it costs nothing in level 1.

Step 2: Level 2 scans a record in sorted order

Level 2 adds a scan that returns every field of a key as text such as city(Pune), sorted by field name, and optionally only the fields that start with a given prefix. CodeSignal describes this level as reusing level 1 while adding requirements and processing data.1

The simplest design sorts at scan time: take the fields of one record, keep those that start with the prefix, sort them, and format them. Its cost is f prefix checks plus k log k to sort the k fields that match, out of f fields in the record (f log f with no prefix), and a set stays constant time. A missing key returns an empty list, not an error. The mistake to avoid is sorting the whole database, which makes every scan cost the size of everything.

def scan(self, key, prefix=""):
    rec = self._d.get(key, {})
    return [f"{f}({v})" for f, (v, e) in sorted((f, ve) for f, ve in rec.items() if f.startswith(prefix))]

Step 3: Level 3 adds a clock without breaking levels 1 and 2

Level 3 adds timestamps and time to live without breaking levels 1 and 2. Each write now carries a timestamp and an optional TTL, and a value written at time t with TTL k is readable at times before t+k and gone from t+k on. CodeSignal says this level assesses supporting advanced features while keeping backward compatibility with existing functionality.1

Backward compatibility is the promise that old calls keep working after new features arrive. The safe move is to add new methods, set_at, get_at, delete_at and scan_at, and leave levels 1 and 2 untouched. Both families share one helper that says whether a stored expiry is still alive at a given time. A value stored without a TTL has an empty expiry and is alive forever, so the old methods keep their meaning. Expired values are not deleted eagerly; reads ignore them, so every operation stays as cheap as before.

The failure to watch is the boundary: at t+k the value is already gone, so the check is "timestamp less than expiry", not "less than or equal".

Step 4: Level 4 backs up and restores without regressions

Level 4 adds backup and restore. A backup at time b saves a snapshot, a frozen copy of every field that is still alive, together with how much of its time to live remains. A restore at time r rebuilds the database from the latest backup taken at or before a given time, and re-bases each remaining time to live onto the restore time. CodeSignal says the last level finalizes the story, enhances functionality without regressions, and also judges code design after the fact.1

Two details decide correctness. Expired fields must not enter the backup, and a field with five ticks left must still have five ticks left after the restore, not five ticks from its original start. The worked example checks both.

Each level of the database task builds on the one beforeA stack of four layers added one at a time: set, get and delete; scan with a prefix; timestamps and time to live; backup and restore. Every new layer reuses the layers below it.Level 1: set, get, deletekey, then field, then valueLevel 2: scanfields in sorted order, optional prefixLevel 3: timestamps and TTLa field expires at set time plus ttlLevel 4: backup and restoresnapshots with remaining time to liveDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/codesignal-in-memory-database.html

Each level reuses the code below it, so a change at level 3 must leave levels 1 and 2 passing.CC BY 4.0 — source: The CodeSignal in-memory database assessment, level by level
Step-by-step description
  1. Store, read and delete fields.
  2. Scan a record in sorted order.
  3. Add a clock and time to live.
  4. Back up and restore the data.
  • Level 1 stores fields under keys with set, get and delete.
  • Level 3 adds timestamps and time to live without breaking levels 1 and 2.

Knowing it is half the job. Saying it is the other half.

Interview Clozo is an AI interview copilot for real interview rounds: it hears the question and puts a strong answer on your screen, and you say it in your own words. See the features or how it works.

Start Free Trial →

Worked example

The database above was run against 20 scripted checks, one per rule in the levels: missing keys, double deletes, prefix scans, the expiry boundary, the old methods after level 3, backups that skip expired fields, and a re-based time to live. All 20 passed.2

The expiry boundary and the backup rules decide the level 3 and level 4 results.
CallResult
set_at(10, s1, token, abc, ttl=5) then get_at(14, s1, token)abc
get_at(15, s1, token)nothing (expired at 10 + 5)
scan_at(14, s1)role(admin), token(abc)
scan_at(15, s1)role(admin)
delete_at(16, s1, token)false (already expired)
backup(20)3 live fields saved (the expired token is skipped)
restore(30, 20) then get_at(30, s1, role)admin (the later change to guest is undone)
a field with ttl 10 set at 0, backup at 4, restore at 100alive at 105, gone at 106 (6 ticks left, re-based)

The last row is the subtle one: at the backup the field had 6 ticks left, so after the restore at 100 it dies at 106, not at its original time.2

A field with a time to live of 5 dies at timestamp 15Timeline from timestamp 10 to 30: a token set at 10 with a time to live of 5 is readable through 14 and gone at 15, a backup is taken at 20 and a restore happens at 30.010152030timestampset, ttl 5expiredbackuprestoretoken readableDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/codesignal-in-memory-database.html
The expiry timestamp is set time plus ttl, and the field is already gone when the clock reaches it.CC BY 4.0 — source: The CodeSignal in-memory database assessment, level by level
Text description of the diagram
  • A token set at timestamp 10 with a time to live of 5 is readable at 14 and gone at 15.
  • A backup at 20 and a restore at 30 re-base any remaining time to live.
The minutes CodeSignal says to expect for the four levels sum to 90 to 165 minutes against a 90-minute limit, so speed on level 1 matters.
LevelMinutes CodeSignal says to expect
110 to 15
220 to 30
330 to 60
430 to 60

These minutes come from CodeSignal's technical brief (August 2021, per the copy in the practice repository), which says candidates are "not necessarily expected to complete all tasks"; a public practice repository (unofficial, undated README) repeats them.3

Trade-offs and edge cases

Sort on read, or keep fields sorted?

Sorting at scan time is the best default. A sorted list kept with bisect finds the insertion point in logarithmic time, but the Python documentation notes that the search is dominated by the linear-time insertion, so a sorted list with bisect costs f per set because insertion shifts elements (Python bisect documentation, version 3.15.1, read Oct 2026). A balanced tree keeps order at log f per set, but Python has no built-in one and the brief lists third-party libraries under what to exclude at levels 2 and 3.3 Keep the simple version, state the cost, and say you would switch only if scans were far more frequent than writes.

Sorting at scan time keeps writes cheapThree ways to return fields in sorted order, compared by the cost of a scan and of a set, where f is the number of fields in one record.ChoiceScanSetSort on scanf log fConstantSorted listffBalanced treeflog fDiagram: Interview Clozo, CC BY 4.0 ·interview.clozo.ai/blog/codesignal-in-memory-database.html
For small records, sorting on read is the simplest choice and the cheapest to write.CC BY 4.0 — source: The CodeSignal in-memory database assessment, level by level
Text description of the diagram
  • Sorting on every scan costs f log f per scan and constant time per set.
  • A sorted list with bisect costs f per set because insertion shifts elements.
  • A balanced tree costs log f per set but Python has no built-in one.

Lazy expiry, or an eager sweep?

Lazy expiry ignores expired values on read, so there is no background work and no ordering bug, but expired values hold memory until overwritten. There are two eager options. A min-heap keyed by expiry time pops due entries at a cost of log n per write. Redis-style sampling runs on a timer: "periodically, Redis tests a few keys at random amongst the set of keys with an expiration", and removes those that have expired (Redis expiry documentation, read October 2026).4 For an assessment, lazy expiry is the right answer; mention the heap or sampling as the production options.

Which version of the task will I get?

You cannot know in advance: CodeSignal does not publish the database task, and reported versions differ at level 4. The levels on this page follow CodeSignal's own description of the framework. Basic implementation comes first, then reuse and data handling, then advanced features with backward compatibility, and last extension without regressions.1 CodeSignal does not publish the database task itself, so the exact operations here are an illustrative project written for this page, in Python.

PracHub describes a published task of this kind as "Design an in-memory database with TTL and backups" (updated April 2026).5 Reported versions differ at level 4: some restore by a backup id instead of by time (PracHub, updated April 2026).5 CodeSignal's brief (August 2021, per the copy in the practice repository) grants partial credit per test passed. Its example task, a file hosting service, adds timestamped _AT versions of the existing methods, where no time to live means the value never expires, and its level 4 rolls back to a timestamp and recalculates every time to live.3

What about the real clock?

The timestamps in this task are numbers the caller passes in. Reading the system clock inside the methods would make behaviour depend on when the test runs, which is hard to check. Pass time in.

How to answer it in an interview

Interviewer asks

Walk me through how you would build an in-memory database with TTL and backup.

A strong answer

I would store each key as a map of fields to a pair of value and expiry, with an empty expiry meaning forever. Set, get and delete are plain dictionary operations. Scan sorts one record's fields and filters by prefix. For TTL I add timestamped versions of the same methods that share one liveness check, so the old methods still work and expired values are ignored on read. Backup saves live fields with their remaining time, and restore re-bases it onto the restore time. I would test the expiry boundary first.

Say the storage shape first, then the cost of each level, then the boundary you would test.

Common mistakes

  • "I will rewrite the class at level 3 so the clock is built in" (wrong). CodeSignal's brief warns against changing existing method signatures, so earlier tests can fail (right: add timestamped methods that share the storage and keep the old ones working).
  • "A value expires at its set time plus TTL, inclusive" (wrong). At that timestamp it is already gone (right: alive only while the timestamp is less than set time plus TTL).
  • "Back up everything, expired or not" (wrong). Restored data then comes back to life (right: save only live fields).
  • "Restore the original expiry times" (wrong). The values would expire too early, often before the restore itself (right: save the remaining time and re-base it).
  • "Sort the whole database on every scan" (wrong). The cost grows with all the data (right: sort only the one record).

Follow-up questions

Design an in-memory database supporting SET, GET, BEGIN, ROLLBACK, COMMIT with nested transaction support?

Keep a stack of overlays, one per open transaction: each begin pushes an empty overlay, writes go to the top overlay, and reads check the overlays from the top down. A delete writes a tombstone, a marker meaning deleted, so a delete inside a transaction hides the old value instead of falling through to it. A nested commit merges its overlay into its parent, a rollback drops it, and only the outermost commit reaches the store.

How do you build a small SQL engine as a parser plus an executor?

Structured Query Language (SQL) is the text language of relational databases. Split the work in two: a parser turns the text into a structure such as a command name with arguments, and an executor runs that structure against the store. Build the executor first with plain method calls, then add the parser on top, so each half can be tested alone.

How do you remove expired keys without scanning every key?

Either sample a few keys with an expiry at random on a timer, as Redis does, or keep a min-heap, a structure that always yields its smallest entry first, of expiry times alongside the data. With the heap, pop entries whose time has passed and delete the matching field only if its stored expiry still equals the heap entry, because a later write may have extended it. This costs log n per write and removes the full scan.

What if one record has a million fields and scans dominate?

Then sorting on every scan costs about f log f each time, so keep the fields sorted with bisect, which costs f per insert because elements shift, or use a balanced tree. The brief excludes binary search and third-party libraries at levels 2 and 3, so say that and state the cost either way.3

How would you make the store safe for several threads?

Protect the dictionary with a lock around each operation, or use one lock per key when contention matters. State that the sorted-list helpers in the standard library are not thread-safe, as the bisect documentation warns, so shared sorted structures need the same protection. See the system design guide for the wider picture.

Design and implement an in-memory key-value store that supports set, transactional begin, commit, and abort operations?

With one open transaction, keep an undo log, a list of each key's old value taken before the first change to it, recording "absent" for a key first created inside the transaction. Abort replays the log backwards to restore those values, and commit throws the log away because the store already holds the new values. An overlay, as in the nested version above, gives the same result without touching the store until commit.

Key takeaways
  • The task is one database class extended through four levels in 90 minutes, and each level must leave the earlier ones passing.
  • Store each value with an expiry slot from level 1, with empty meaning forever.
  • Add timestamped methods and never change old signatures; that is how levels 1 and 2 keep passing.
  • Backups skip expired fields and keep the remaining time, which a restore re-bases.
  • Say the cost of each choice: sort on scan costs f log f, and lazy expiry costs nothing on write.

Sources

  1. Industry Coding Framework datasheet — CodeSignal (document shows no date; read 2026-10-24), 24 Oct 2026
  2. CodeSignal Practice Industry Coding Framework (README) — GitHub repository, unofficial practice material (page shows no date; read 2026-10-24), 24 Oct 2026
  3. Industry Coding Skills Evaluation Framework (technical brief) — CodeSignal (August 2021, per the copy in the practice repository; the PDF itself shows no date), 1 Aug 2021
  4. Design an in-memory database with TTL and backups — PracHub (page updated 2026-04-20), 20 Apr 2026
  5. Key expiry, Redis documentation, section How Redis expires keys — Redis (page shows no date; read October 2026), 24 Oct 2026
  6. bisect, array bisection algorithm, Python 3.15.1 documentation — Python Software Foundation (page shows version only; read 2026-10-24), 24 Oct 2026