← Back to blog Ramp OA: CodeSignal Banking in Four Levels
Ramp

Ramp OA: CodeSignal Banking in Four Levels

2026-08-08

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:

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:

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:


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