Persona Identities, a rising Y Combinator company in the identity verification space, is hiring aggressively for Backend / Full-stack roles in 2026. Their OA does NOT follow the LeetCode style — they use CodeSignal's "four-level progressive task" framework, where one prompt unlocks four levels and your earlier code must remain compatible with later levels. The classic prompt is the "In-Memory Database", now nearly a Persona OA signature, also seen at Ramp and Square.
This article is based on a recent Persona OA debrief from candidates. We'll break down Levels 1-4 with incrementally refactorable Python implementations, and call out exactly where each level traps people.
Persona OA Overview
| Dimension | Detail |
|---|---|
| Platform | CodeSignal General Coding Framework |
| Duration | 90 minutes (IDE familiarization included) |
| Question | 1 prompt × 4 progressive levels |
| Test cases | ~25-30 total |
| Language | Python / Java / Go / JS — Python has highest pass rate |
| Scoring | 100 pts (L1=20, L2=25, L3=25, L4=30) |
CodeSignal's nuance: each level only reveals its own tests, and you must pass the current level to unlock the next. If you hard-code value types in Level 1, you'll be forced into a painful rewrite by Level 3.
Level 1: Basic Key-Value Operations
Problem
Implement an in-memory database supporting:
SET <key> <field> <value>— store a field under a recordGET <key> <field>— return value, or empty string if missingDELETE <key> <field>— delete field; return"true"or"false"
All return values must be strings.
Approach
Natural two-level dict: db[key][field] = value. The key trick is do not create empty dicts eagerly — that pollutes backups in Level 4.
Python Solution
class InMemoryDB:
def __init__(self):
self.db = {}
def set(self, key, field, value):
self.db.setdefault(key, {})[field] = value
return ""
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"
Time: amortized O(1) per op Space: O(K·F)
Level 2: Scan and Prefix Query
Problem
Add:
SCAN <key>— return allfield(value)entries under key, sorted by field, comma-separatedSCAN_BY_PREFIX <key> <prefix>— only prefix-matched entries
Approach
Just sorted(items()). Do NOT pre-sort using a SortedDict — Level 4 will do bulk rollbacks, so simpler structures win.
Python Solution
def scan(self, key):
if key not in self.db:
return ""
items = sorted(self.db[key].items())
return ", ".join(f"{f}({v})" for f, v in items)
def scan_by_prefix(self, key, prefix):
if key not in self.db:
return ""
items = sorted(
(f, v) for f, v in self.db[key].items() if f.startswith(prefix)
)
return ", ".join(f"{f}({v})" for f, v in items)
Time: O(F log F)
Level 3: TTL Expiration
Problem
Every command now carries a timestamp:
SET_AT <key> <field> <value> <ts>— permanent writeSET_AT_WITH_TTL <key> <field> <value> <ts> <ttl>— expires atts + ttlDELETE_AT,GET_AT,SCAN_AT,SCAN_BY_PREFIX_AT— skip expired entries
Expiration is strict: ts >= expire_at means expired.
Approach
Change value from string to (value, expire_at) where expire_at = None means permanent. Route every read through a single helper _alive(key, field, ts) so the check isn't scattered across six methods.
Python Solution
class InMemoryDB:
def __init__(self):
self.db = {} # key -> field -> (value, expire_at|None)
def _alive(self, key, field, ts):
rec = self.db.get(key, {}).get(field)
if rec is None:
return None
value, expire_at = rec
if expire_at is not None and ts >= expire_at:
return None
return value
def set_at(self, key, field, value, ts):
self.db.setdefault(key, {})[field] = (value, None)
return ""
def set_at_with_ttl(self, key, field, value, ts, ttl):
self.db.setdefault(key, {})[field] = (value, ts + ttl)
return ""
def get_at(self, key, field, ts):
v = self._alive(key, field, ts)
return "" if v is None else v
def delete_at(self, key, field, ts):
if self._alive(key, field, ts) is None:
return "false"
del self.db[key][field]
return "true"
def scan_at(self, key, ts):
if key not in self.db:
return ""
live = sorted(
(f, v) for f, (v, exp) in self.db[key].items()
if exp is None or ts < exp
)
return ", ".join(f"{f}({v})" for f, v in live)
Pitfall: many people eagerly evict expired entries — this loses data when Level 4 restores to a past timestamp. Lazy expiration is correct.
Level 4: Backup & Restore
Problem
BACKUP <ts>— snapshot atts, return count of non-expired keys as a stringRESTORE <ts> <backup_ts>— restore state frombackup_ts, but all TTLs must be recalculated against currentts
Approach
Two critical bits:
- Snapshot format: not a shallow copy. Store
(field, value, remaining_ttl)whereremaining_ttl = expire_at - backup_ts; permanent entries useNone. - Restore recomputes expire_at:
new_expire = ts + remaining_ttl
Most failures: copying the raw expire_at back — every restored TTL instantly expires.
Python Solution
class InMemoryDB:
def __init__(self):
self.db = {}
self.backups = {} # backup_ts -> snapshot
# ... reuse Level 1-3 methods ...
def backup(self, ts):
snapshot = {}
for key, fields in self.db.items():
live = {}
for f, (v, exp) in fields.items():
if exp is None:
live[f] = (v, None)
elif ts < exp:
live[f] = (v, exp - ts)
if live:
snapshot[key] = live
self.backups[ts] = snapshot
return str(len(snapshot))
def restore(self, ts, backup_ts):
if backup_ts not in self.backups:
return ""
snap = self.backups[backup_ts]
new_db = {}
for key, fields in snap.items():
new_fields = {}
for f, (v, remaining) in fields.items():
exp = None if remaining is None else ts + remaining
new_fields[f] = (v, exp)
new_db[key] = new_fields
self.db = new_db
return ""
Level 4 Common Mistakes
- Using
copy.deepcopy(self.db)as the snapshot — drags expired entries back in - Not recalculating
expire_aton restore — every TTL is stale - Letting
SET_ATmutate prior backups — backups must stay read-only - Iterating twice (filter expired, then compute remaining) — merge into one pass
Persona Prep Roadmap
| Stage | Recommended Practice |
|---|---|
| CodeSignal warm-up | LC 146 LRU Cache, LC 460 LFU Cache |
| State machines | LC 432, LC 1206 Skiplist |
| TTL thinking | Redis EXPIRE docs + LC 362 Hit Counter |
| Snapshot/restore | git-style incremental snapshots, LC 1166 File System |
Pacing: nail Level 1 in 15 min, Levels 2-3 in 20-25 min each, leave 25-30 min for Level 4. Don't over-abstract Level 1 — CodeSignal scores on test cases, not "clean code."
FAQ
Q1: How hard is Persona OA vs LeetCode Medium?
Per-question difficulty does not exceed LC Medium, but the real test is incremental refactoring — keeping earlier code compatible across four levels under a 90-min clock. If you hard-code value types in Level 1, Level 3 becomes brutal.
Q2: What platform and duration?
CodeSignal General Coding Framework, 90 minutes. Note: there is also a separate 60-min General Coding Assessment (GCA) earlier in the funnel — that one is pure LeetCode-style. In-Memory Database is the Practical Task that follows.
Q3: Are Backend and Full-stack OAs different?
The OA itself is identical. Full-stack adds a React/TypeScript practical after OA, while backend goes into system design. No need to specialize at OA stage.
Q4: Which language has the highest pass rate?
Python > Go > Java > JS. CodeSignal's Python template plus dict-of-dicts ergonomics minimize syntax overhead. Avoid Java — nested generic Map types eat a huge chunk of your 90 minutes.
Q5: What if backup_ts in RESTORE doesn't exist?
The problem statement guarantees the backup_ts was previously created — but restoring an older backup after several newer ones is a sneaky hidden case. Use a dict keyed by ts, not a list.
Q6: How fast does Persona move after OA?
Most 1point3acres reports say 3-7 business days to the next round. Persona moves fast — hiring managers often send calendar invites directly. Check your spam folder.
Preparing Persona / Ramp / Square CodeSignal-style four-level OAs?
The four-level format tests engineering judgment and state management, not algorithmic ceiling. If you want our curated four-level question bank (Persona, Ramp, Stripe, Square, Brex) or a 1-on-1 walkthrough of Level 3-4 state machines, reach out.
Add WeChat Coding0201 or contact us.
Contact
Email: [email protected] Telegram: @OAVOProxy