ZipRecruiter's 2026 New Grad OA doesn't test algorithms—it's all about design discipline. One big problem, four progressive levels: CRUD, Scan & Prefix, TTL expiry, Backup / Restore. Each level alone is easy, but all four must share one data structure—taking shortcuts in earlier levels triples your refactoring cost. This post lays out the optimal data structures, full Python solutions, and CodeSignal-specific defenses for each level.
ZipRecruiter NG OA Overview
| Dimension | Detail |
|---|---|
| Platform | CodeSignal Test |
| Duration | 90 minutes |
| Questions | 1 problem, 4 levels |
| Difficulty | LeetCode Easy~Medium, design-heavy |
| Pass threshold | 80% tests green |
| Rubric | Correctness + level completion |
Problem: In-memory Database
Build an in-memory key-value store with nested fields:
db[key][field] = value
Operations either act on a key whole or a (key, field) pair.
Level 1: Basic CRUD
Operations:
set(key, field, value)get(key, field)→ value orNonedelete(key, field)→True/False
class Database:
def __init__(self):
self.db: dict = {}
def set(self, key, field, value):
self.db.setdefault(key, {})[field] = value
def get(self, key, field):
return self.db.get(key, {}).get(field)
def delete(self, key, field):
if key in self.db and field in self.db[key]:
del self.db[key][field]
if not self.db[key]:
del self.db[key]
return True
return False
Level 1 keys:
deletereturns boolean—never raise- After deleting the last field of a key, drop the key—otherwise Level 2's scan returns an empty key
Level 2: Scan & Prefix Filtering
Operations:
scan(key)→ all"field(value)"strings in lex orderscan_by_prefix(key, prefix)→ only fields starting with prefix
def scan(self, key):
if key not in self.db:
return []
return [f"{f}({v})" for f, v in sorted(self.db[key].items())]
def scan_by_prefix(self, key, prefix):
if key not in self.db:
return []
return [
f"{f}({v})"
for f, v in sorted(self.db[key].items())
if f.startswith(prefix)
]
Hidden cases:
- Missing key →
[] - Empty prefix → behaves like scan
- Prefix is case-sensitive
Level 3: TTL (Time-to-Live)
Operations:
set_at(key, field, value, timestamp)set_at_with_ttl(key, field, value, timestamp, ttl)get_at(key, field, timestamp)delete_at(key, field, timestamp)scan_at(key, timestamp)/scan_by_prefix_at(...)
Core design: store each field as a (value, start, ttl) tuple; ttl=None means permanent.
def _is_alive(self, entry, timestamp):
value, start, ttl = entry
if ttl is None:
return True
return timestamp < start + ttl
def set_at(self, key, field, value, timestamp):
self.db.setdefault(key, {})[field] = (value, timestamp, None)
return ""
def set_at_with_ttl(self, key, field, value, timestamp, ttl):
self.db.setdefault(key, {})[field] = (value, timestamp, ttl)
return ""
def get_at(self, key, field, timestamp):
if key not in self.db or field not in self.db[key]:
return None
entry = self.db[key][field]
if not self._is_alive(entry, timestamp):
return None
return entry[0]
def delete_at(self, key, field, timestamp):
if key not in self.db or field not in self.db[key]:
return False
entry = self.db[key][field]
if not self._is_alive(entry, timestamp):
return False
del self.db[key][field]
if not self.db[key]:
del self.db[key]
return True
def scan_at(self, key, timestamp):
if key not in self.db:
return []
alive = [
(f, e[0]) for f, e in self.db[key].items()
if self._is_alive(e, timestamp)
]
return [f"{f}({v})" for f, v in sorted(alive)]
Three Level-3 traps:
scan_atmust filter, not delete expired fields—deletion breaks subsequent set_at_with_ttl semanticsdelete_aton expired field returnsFalse—expired ≡ "not present"- TTL is strict less-than:
timestamp < start + ttl, not<=
Level 4: Backup / Restore
Operations:
backup(timestamp)→ snapshot current DBrestore(timestamp, target_timestamp)→ restore the latest backup ≤target_timestamp; on restore, all surviving TTLs are re-anchored to the current timestamp
def __init__(self):
self.db: dict = {}
self.backups: list = []
def _snapshot(self, timestamp):
snap = {}
for key, fields in self.db.items():
snap_fields = {}
for f, (v, start, ttl) in fields.items():
if ttl is None:
snap_fields[f] = (v, None, None)
continue
remaining = start + ttl - timestamp
if remaining > 0:
snap_fields[f] = (v, timestamp, remaining)
if snap_fields:
snap[key] = snap_fields
return snap
def backup(self, timestamp):
self.backups.append((timestamp, self._snapshot(timestamp)))
return str(sum(len(fs) for fs in self.backups[-1][1].values()))
def restore(self, timestamp, target_timestamp):
candidate = None
for bk_ts, snap in self.backups:
if bk_ts <= target_timestamp:
candidate = (bk_ts, snap)
if candidate is None:
self.db = {}
return ""
bk_ts, snap = candidate
self.db = {}
for key, fields in snap.items():
for f, (v, start, ttl) in fields.items():
if ttl is None:
self.db.setdefault(key, {})[f] = (v, timestamp, None)
else:
self.db.setdefault(key, {})[f] = (v, timestamp, ttl)
return ""
Level 4 designs:
- Snapshot only live fields—expired ones cannot be restored
- On restore, re-anchor TTLs to the restore timestamp (this is the level's signature trick)
- If no eligible backup exists, wipe the DB instead of keeping current state
Strategy
1) Level priority: 1 → 2 → 3 → 4
Each level is ~25% of total score. Hitting 100% on Levels 1+2 already puts you in CodeSignal's top 30%. Level 3 is the difficulty cliff—TTL semantics, if wrong, fail the entire level. Level 4 is the stretch goal.
2) Pre-commit to the data structure
Don't store db[key][field] = value in Level 1 and refactor to (value, start, ttl) in Level 3—you'll rewrite everything. Start with the tuple from Level 1, defaulting to (value, 0, None) for permanent fields.
3) Return types matter
CodeSignal scoring is strict about None vs "" vs True/False. Read the prompt: get_at returns None, set_at returns "", delete_at returns boolean.
FAQ
What's the ZipRecruiter OA pass rate?
Overall ~40-50%. Candidates who nail Levels 1+2 pass at ~70%. Level 3 is the elimination zone—TTL semantic slips lose 25% of total score.
Can I debug on CodeSignal?
Yes. CodeSignal supports stdout printing and custom test runs. After each level, run 3-5 boundary cases: empty key, empty prefix, TTL boundary, backup-then-delete.
How long to finish all four levels?
A trained candidate: 60-75 minutes. Recommended split: 10 min planning → 20 min L1+L2 → 30 min L3 → 15 min L4. If L4 doesn't fit, securing full marks on L3 still lands you in the final round.
Are there similar problems elsewhere?
Yes—4-level CodeSignal database problems are nearly identical across Snowflake, Optiver, Capital One, Roku. The "prefix + TTL" template is CodeSignal's standard; only the surface story changes (DB → warehouse → user system).
What comes after the OA?
OA → HR phone screen → 2-3 technical onsite rounds (1 Behavioral + 1-2 Coding + 1 System Design for senior roles). Typical OA-to-onsite turnaround: 2-4 weeks.
Preparing for the ZipRecruiter NG OA?
oavoservice covers the entire CodeSignal in-memory database family: level decomposition, TTL design templates, backup/restore algorithms. We have full walkthroughs for ZipRecruiter, Snowflake, Optiver, and similar 4-level design problems, and can customize practice to your target company.
Add WeChat Coding0201 to book ZipRecruiter OA coaching.
#ZipRecruiter #ZipRecruiterOA #NewGrad #CodeSignal #InMemoryDB #OARealQuestions
Contact
Email: [email protected]
Telegram: @OAVOProxy