I recently took Ramp's OA, which runs on CodeSignal: 90 minutes, four problems split into four progressively unlocked levels. It is not the kind of algorithm puzzle you crack at a glance. It is a progressively-built system-design implementation: you extend a banking system from the simplest deposits and transfers, step by step, into a full service that supports ranking, scheduled payments, and cancellation. This debrief walks through the whole flow, the requirements at each level, and my approach. If you are prepping for a similar OA, I hope this note helps you think through pacing and the data model up front. If you want OA assist or OA live support, the contact details are at the end.
Environment and Format Overview
Worth stating first: this 90-minute CodeSignal banking format draws from a shared bank. The same style shows up in OAs at Meta, Coinbase, and TradeDesk, and there are currently three big scenarios circulating, of which the banking system is only one. So getting familiar with this "build it up level by level" style ahead of time pays off more than grinding brand-new problems on the spot.
| Item | Details |
|---|---|
| Platform | CodeSignal |
| Total time | 90 minutes |
| Count | 4 problems, one per level |
| Format | Progressively unlocked: you must fully pass the current level before the next opens |
| Focus | Data-structure modeling, incremental extension, timestamp-ordered processing, heaps and hash maps |
The key rule: you must pass every test in the current level before the next level unlocks. So make level 1 rock solid, and don't leave a hidden bug just to rush ahead. Once a level blocks you, all the points beyond it are locked. Also, every operation carries a unique, strictly-increasing timestamp, which is the basis for the ordering and scheduling in the later levels.
Level 1: A Simple Banking System
Background
The system starts with no accounts. You need to support three operations:
createAccount(timestamp, accountId): create the account if it does not exist and returnTrue; returnFalseif it already exists.deposit(timestamp, accountId, amount): deposit and return the updated balance; return empty (None) if the account does not exist.transfer(timestamp, sourceId, targetId, amount): transfer between two different accounts, returning the source's remaining balance on success; return empty (None) if either account does not exist, the two accounts are the same, or the source has insufficient funds.
Approach
A HashMap of accountId -> balance is enough, and all three operations are O(1). At this level the timestamp only orders operations; it does not affect the logic.
Python Solution
from typing import Optional
class BankingSystem:
def __init__(self) -> None:
# accountId -> balance
self.accounts: dict[str, int] = {}
def create_account(self, timestamp: int, account_id: str) -> bool:
"""Create an account; return False if it already exists."""
if account_id in self.accounts:
return False
self.accounts[account_id] = 0
return True
def deposit(self, timestamp: int, account_id: str,
amount: int) -> Optional[int]:
"""Deposit and return the latest balance; None if missing."""
if account_id not in self.accounts:
return None
self.accounts[account_id] += amount
return self.accounts[account_id]
def transfer(self, timestamp: int, source_id: str, target_id: str,
amount: int) -> Optional[int]:
"""Return the source's remaining balance on success, else None."""
if source_id not in self.accounts or target_id not in self.accounts:
return None
if source_id == target_id: # cannot transfer to self
return None
if self.accounts[source_id] < amount: # insufficient funds
return None
self.accounts[source_id] -= amount
self.accounts[target_id] += amount
return self.accounts[source_id]
Time complexity: createAccount / deposit / transfer are all O(1). Space complexity: O(k), where k is the number of accounts.
Level 2: Top Spenders
Background
Add topSpenders(timestamp, n), returning the top n accounts by cumulative outgoing amount. Outgoing includes successful transfers out, plus the scheduled payments / withdrawals that execute successfully in the later levels. Sort by outgoing descending, break ties by accountId ascending, and format as "accountId(totalOutgoing)"; if there are fewer than n accounts, return all of them.
Approach
Keep an outgoing field per account (initialized to 0 on creation) and increment it only on the source's side of a successful transfer. Deposits and incoming transfers do not count. topSpenders iterates all accounts and sorts.
Python Solution
class BankingSystem:
def __init__(self) -> None:
self.accounts: dict[str, int] = {}
self.outgoing: dict[str, int] = {} # accountId -> cumulative out
def create_account(self, timestamp: int, account_id: str) -> bool:
if account_id in self.accounts:
return False
self.accounts[account_id] = 0
self.outgoing[account_id] = 0 # outgoing starts at 0
return True
def transfer(self, timestamp: int, source_id: str, target_id: str,
amount: int) -> Optional[int]:
if source_id not in self.accounts or target_id not in self.accounts:
return None
if source_id == target_id or self.accounts[source_id] < amount:
return None
self.accounts[source_id] -= amount
self.accounts[target_id] += amount
self.outgoing[source_id] += amount # only the source counts
return self.accounts[source_id]
def top_spenders(self, timestamp: int, n: int) -> list[str]:
"""Return the top n accounts by cumulative outgoing."""
ranked = sorted(
self.accounts.keys(),
key=lambda acc: (-self.outgoing[acc], acc), # out desc, id asc
)
return [f"{acc}({self.outgoing[acc]})" for acc in ranked[:n]]
Time complexity: topSpenders is O(m log m) per query, m being the account count; the other operations remain O(1). Space complexity: O(m).
Level 3: Scheduled Payments and Cancellation
Background
Add two operations:
schedulePayment(timestamp, accountId, amount, delay): create a payment that executes attimestamp + delayand return a globally increasing paymentId; return empty (None) if the account does not exist. If the balance is insufficient at execution time, skip that payment. A payment that executes successfully counts toward outgoing.cancelPayment(timestamp, accountId, paymentId): can only cancel a payment that has not executed, has not been cancelled, and belongs to the given account; otherwise returnFalse.
Two key timing rules: payments due at a given time must execute before any other operation at that timestamp, and multiple payments due at the same time execute in creation order.
Approach
Use a min-heap keyed on (executionTime, creationOrder) plus a HashMap of paymentId -> details/status. Before any public operation, process all payments with executionTime <= current timestamp: skip cancelled ones; if funds suffice, deduct and add to outgoing, otherwise mark it failed. Creating a payment generates an increasing ID and writes to both the heap and the map; cancel locates by ID and checks ownership and status.
Python Solution
import heapq
class BankingSystem:
def __init__(self) -> None:
self.accounts: dict[str, int] = {}
self.outgoing: dict[str, int] = {}
self.pay_heap: list[tuple[int, int, str]] = [] # (exec_time, order, id)
self.payments: dict[str, dict] = {} # paymentId -> details
self.payment_counter = 0 # globally increasing paymentId
self.order_counter = 0 # creation order for same-time ties
def _process_due(self, timestamp: int) -> None:
"""Execute all payments with executionTime <= timestamp."""
while self.pay_heap and self.pay_heap[0][0] <= timestamp:
_, _, payment_id = heapq.heappop(self.pay_heap)
info = self.payments[payment_id]
if info["status"] != "pending":
continue # cancelled, skip
acc, amount = info["account_id"], info["amount"]
if self.accounts.get(acc, 0) >= amount:
self.accounts[acc] -= amount
self.outgoing[acc] += amount # successful payment counts
info["status"] = "done"
else:
info["status"] = "failed" # insufficient funds, skip
def schedule_payment(self, timestamp: int, account_id: str,
amount: int, delay: int) -> Optional[str]:
self._process_due(timestamp)
if account_id not in self.accounts:
return None
self.payment_counter += 1
payment_id = f"payment{self.payment_counter}"
self.order_counter += 1
self.payments[payment_id] = {
"account_id": account_id,
"amount": amount,
"status": "pending",
}
heapq.heappush(
self.pay_heap,
(timestamp + delay, self.order_counter, payment_id),
)
return payment_id
def cancel_payment(self, timestamp: int, account_id: str,
payment_id: str) -> bool:
self._process_due(timestamp)
info = self.payments.get(payment_id)
if info is None or info["account_id"] != account_id:
return False
if info["status"] != "pending": # already executed or cancelled
return False
info["status"] = "cancelled"
return True
Time complexity: heap push / pop is O(log n), n being the number of pending payments; cancel is roughly O(1) via the map lookup. The _process_due call before each public operation is O(log n) amortized.
Space complexity: O(n) for the heap and the payment table.
Level 4: Common Variants and Strategy
On my OA, Level 4 only showed the title and I never got a good look at the full spec, so I will not invent an exact problem statement or fake code here, only be honest about the direction. This CodeSignal banking system's Level 4 typically extends the first three levels further. Typical Level 4 variants include: balance history queries (looking up an account's balance at a given timestamp), merchant cashback with expiring rules, and account merging (folding one account's balance, outgoing record, and pending payments into another).
For an unseen Level 4, the general strategy is: reuse the Level 1-3 data model (the account table, the outgoing field, the payment heap), keep the "process timestamp-ordered events, settle due payments before each public operation" backbone, then read the spec carefully and attach the new rules to the existing structure. As long as the first three levels are abstracted cleanly, the fourth is usually a natural extension rather than a rewrite.
Prep Tips
Because these problems come from a shared bank, internalizing the problem type beats grinding endless new problems:
- Nail Level 1: fix the data model, since the next three levels all add fields on top of it. Abstracting an account as "balance + outgoing + related payments" early makes later extensions much smoother.
- Timestamp is the backbone: from Level 3 on, settle due payments before any public operation, and don't miss that step at some entry point.
- Incremental, not rewrite: each level adds the smallest change, preserving the logic that already passed, so you never break earlier test cases.
- Know the three big scenarios: the banking system is only one of them, so run through the patterns of the other two as well, and you can slot in fast on the spot.
FAQ
Q1: Does Ramp's OA really require every level to be fully correct to continue?
Yes. This CodeSignal format is progressively unlocked: the next level opens only after every test in the current level passes. So it is better to spend a few extra minutes confirming edge cases at a lower level than to leave a hidden bug that locks the points beyond it.
Q2: Is 90 minutes enough for four problems?
Yes, provided you get the data model right from the start. The four problems are layered extensions of the same system, so if Level 1's abstraction is sound, the rest is mostly adding fields and methods. If the early structure is messy, the heap and scheduling at Level 3 get painful.
Q3: In topSpenders, which amounts actually count as outgoing?
Only the source's successful transfers, plus the scheduled payments / withdrawals that later execute successfully. Deposits and incoming transfers do not count. Equal amounts sort by accountId ascending, and the output format is accountId(totalOutgoing).
Q4: Why does the execution timing of scheduled payments matter so much?
Because due payments must execute before any other operation at the same timestamp, and multiple payments due at the same time process in creation order. A min-heap keyed on (executionTime, creationOrder) satisfies this order naturally, and calling settlement once at each public entry point means you never miss one.
Q5: Will I see this banking system problem at other companies?
Yes. This 90-minute CodeSignal format draws from a shared bank, and a similar style shows up at Meta, Coinbase, and TradeDesk, with three big scenarios currently circulating. Running through the patterns ahead of time carries over across companies.
Prepping for Ramp's or another company's CodeSignal OA? We know this 90-minute progressively-unlocked banking format well, from data modeling to scheduled-payment scheduling, and can help you sort it all out, with end-to-end OA assist and OA live support.
Add WeChat Coding0201 now to get a one-on-one custom prep plan.
Contact
- WeChat: Coding0201
- Email: [email protected]
- Telegram: @OAVOProxy