How to design a ledger for Coinbase interviews

AAcePrompt Team·August 21, 2026·11 min read
How to design a ledger for Coinbase interviews

Interviewing for a Senior Software Engineer role at Coinbase reveals one core truth about their engineering culture: absolute correctness trumps everything else. When you are managing billions of dollars in crypto and fiat transactions, a dropped message or a subtle race condition is not just a bug. It is a catastrophic financial loss. Because of this, the system design round frequently asks you to design a highly concurrent, fault-tolerant double-entry ledger. Forget your standard web-scale system design interview, where eventual consistency and clever caching usually save the day. In this room, consistency is king. You have to prove you can build an architecture that never loses a single cent, even while absorbing massive transaction spikes during a crypto bull run.

The Coinbase Senior SWE Interview Process

Before diving into the architecture, you need to understand how Coinbase evaluates candidates. The system design round is heavily weighted toward domain execution and defensive engineering. They do not just want to see boxes and arrows on a whiteboard; they want to see you identify bottlenecks, handle edge cases, and choose the right database isolation levels. You need to demonstrate mechanical sympathy—an understanding of how your software interacts with the underlying hardware, database locks, and network partitions.

RoundFocusDuration
Recruiter ScreenBackground and culture fit30 mins
Technical ScreenData structures and algorithms60 mins
System DesignArchitecture, trade-offs, and scalability60 mins
Domain / ExecutionDeep dive into past projects and technical depth60 mins
BehavioralValues and cross-functional collaboration60 mins

Deep Dive: Designing a Double-Entry Ledger

A Brief Primer on Double-Entry Accounting for Engineers

Before you can design the system, you have to understand the domain. In double-entry accounting, money is never created or destroyed; it only moves between accounts. Every transaction must have at least two legs (entries). A debit increases an asset account or decreases a liability account, while a credit does the opposite. The golden rule is that the sum of all debits and credits for a single transaction must always equal exactly zero.

How to design a ledger for Coinbase interviews

Look at a concrete example: a user buying 1 BTC for $50,000. In your system, you do not just deduct $50,000 from their USD wallet and add 1 BTC to their crypto wallet. You have to route this through internal exchange accounts. The transaction would look like this: Debit User USD Wallet $50,000. Credit Exchange USD Revenue Account $50,000. Debit Exchange BTC Inventory Account 1 BTC. Credit User BTC Wallet 1 BTC. This multi-leg structure ensures that the platform's total assets and liabilities are always perfectly balanced. If an auditor wants to know exactly where the money went, the ledger provides an unbroken, mathematically verifiable chain of custody.

What are the functional and non-functional requirements?

By definition, a double-entry ledger requires every transaction to have at least two entries that sum exactly to zero. On paper, the functional requirements look straightforward enough. You will need to create accounts, process multi-leg transactions atomically, and retrieve account balances. But the non-functional requirements are where this interview gets brutal. You need strict ACID guarantees alongside high availability. You also have to handle high-concurrency hotspots, such as a corporate account receiving thousands of deposits per second. Do not even think about relying on eventual consistency for the core ledger. You must architect for strong consistency, absolute immutability, and exactly-once processing.

  • Strict ACID compliance: The database must guarantee atomicity and durability for every multi-leg transaction.
  • Serializable isolation: Prevent race conditions, write skew, and phantom reads.
  • Immutability: The ledger must be an append-only log. No UPDATE or DELETE statements are allowed on entries.
  • High Availability: Target 99.99% uptime, ensuring the system can survive node failures without data loss.
  • Exactly-once processing: Strict idempotency controls to prevent double-charging users during network retries.

How do you structure an immutable double-entry accounting schema?

A common mistake candidates make is treating the account balance as the source of truth. In a true financial ledger, the balance is merely a materialized view used for fast reads. The actual source of truth is the immutable log of entries. If your database crashes and you lose the accounts table, you should be able to reconstruct every user's exact balance from the entries table. You need three core tables to make this work: Accounts, Transactions, and Entries.

The Transactions table groups multiple entries together to ensure they commit atomically. The Entries table records the actual debits and credits. Every entry must reference a transaction ID and an account ID. Notice the version column on the Accounts table in the schema below. This is your first line of defense against race conditions, setting you up for optimistic concurrency control. Also, the entries table is strictly append-only. If a transaction needs to be reversed (like a bounced ACH transfer), you insert a new compensatory transaction that negates the original amounts rather than deleting the old rows.

Table NameColumnsPurpose
Accountsid, balance, currency, versionStores the materialized balance for fast reads and version for optimistic locking.
Transactionsid, idempotency_key, status, created_atGroups entries together and enforces exactly-once processing via unique constraints.
Entriesid, transaction_id, account_id, amount, directionThe immutable source of truth. Records the debit or credit amount.

Choosing the Right Database Isolation Level

When you are writing to the ledger, the database isolation level you choose can make or break the system. Most relational databases default to Read Committed, which is incredibly dangerous for financial applications. Under Read Committed, your transaction only sees data committed before the query began. This opens the door to race conditions like lost updates or write skew. Imagine two concurrent transactions trying to withdraw $100 from an account with a $150 balance. If both transactions read the balance at the exact same millisecond, they both see $150. They both verify the user has sufficient funds, and they both proceed to deduct $100. The account ends up with a negative balance of -$50, and your exchange just lost money.

To prevent this, you must use the Serializable isolation level. Serializable ensures that concurrent transactions yield the exact same database state as if they were executed sequentially, one after the other. While this provides the strongest possible correctness guarantees, it comes with a massive performance penalty. Serializable transactions frequently fail with serialization anomalies under high contention, requiring your application layer to implement robust exponential backoff and retry logic.

Idempotency: Guaranteeing Exactly-Once Processing

In distributed systems, network partitions and timeouts are guaranteed. Imagine a scenario where a downstream payment gateway processes a $500 withdrawal, but the HTTP response times out before reaching your ledger. If the client retries the request, your system might process the withdrawal twice. To prevent this, you must implement strict idempotency. Every incoming request must include a unique idempotency key generated by the client (usually a UUID v4).

When the ledger receives a request, it first checks the Transactions table for this key. You enforce this at the database level using a UNIQUE constraint on the idempotency_key column. If a duplicate request arrives, the database rejects the insert, and your application can safely return the result of the original transaction. You also need to track the state of the transaction. A transaction might be PENDING, COMPLETED, or FAILED. If a retry comes in while the original transaction is still PENDING, your system should return a 409 Conflict or a 202 Accepted, signaling the client to poll for the result rather than initiating a new transfer.

A classic interview trap is relying on standard database transactions with pessimistic row locks. Think about an exchange hot wallet receiving thousands of deposits a second. In that scenario, row-level locking (like SELECT FOR UPDATE) causes massive contention. The database connection pool fills up with blocked queries, leading to inevitable timeouts and cascading failures across your entire infrastructure. You will want to discuss two senior-level alternatives instead.

The first is optimistic concurrency control (OCC), which uses the version number on the account balance we defined earlier. If a transaction reads version 5 but attempts to write when the version has already bumped to 6, the database rejects the update, and the application layer simply retries. OCC handles low-contention accounts perfectly because it avoids the overhead of locking. However, it falls apart under heavy load due to constant retry loops.

For extreme hotspots, a much better approach is a single-threaded execution loop. Think along the lines of the LMAX Disruptor architecture. When you route all transactions for a specific account partition to a single thread, you completely eliminate lock contention. The system processes transactions sequentially in memory, updates the balances, and then batch-commits them to the database. By aligning your software architecture with the underlying hardware, you can process millions of transactions per second without a single database lock.

Tip: Make sure to bring up the Outbox Pattern when explaining how the ledger talks to downstream services like notifications or analytics. If you write the transaction and the outbound event to the same database in a single atomic commit, you guarantee the system never fires off a notification for a failed transaction.

Decoupling the Ledger with the Outbox Pattern

The core ledger should do one thing: record financial truth. It should not be making synchronous HTTP calls to an email service to send a receipt. If the email service goes down, your ledger goes down. Instead, you need an asynchronous event-driven architecture. But how do you guarantee an event is published if and only if the database transaction commits? This is where the Transactional Outbox pattern shines.

Inside the same database transaction that inserts the ledger entries, you insert an event payload into a dedicated outbox table. Because both inserts happen within the same ACID transaction, they either both succeed or both fail. Next, you run a separate background process, often a Change Data Capture (CDC) tool like Debezium, that tails the database Write-Ahead Log (WAL). When Debezium sees a new row in the outbox table, it publishes the event to a Kafka topic. This completely decouples your ledger from downstream consumers while maintaining strict at-least-once delivery guarantees.

How do you achieve horizontal scale and consensus-based replication?

Eventually, a single relational database hits a hard ceiling. To scale horizontally, you have to shard the ledger. Sharding by account ID is the most common route, though it immediately introduces the headache of distributed transactions whenever a transfer spans two different shards. For example, if Alice on Shard A sends money to Bob on Shard B, both shards must commit atomically. You could solve this with a Two-Phase Commit (2PC) protocol, but that is notoriously slow and prone to blocking. If the coordinator node dies after the prepare phase, the participating databases are left holding locks indefinitely, freezing account balances.

A more modern strategy relies on a distributed consensus algorithm like Raft or Paxos. Tell your interviewer you would opt for a NewSQL database like Spanner or CockroachDB that handles distributed ACID transactions natively. These systems use highly synchronized clocks (like Google's TrueTime) and consensus groups to manage replication and consistency across shards without the crippling overhead of traditional two-phase commits. That way, you get to focus your application logic on core business rules rather than wrestling with distributed locks and coordinator failures.

The Ultimate Safety Net: Continuous Reconciliation

No matter how bulletproof your architecture seems on the whiteboard, software has bugs. Hardware fails. Cosmic rays flip bits. In a financial system, you cannot just trust that the application logic worked perfectly. You need an independent verification mechanism. This is where continuous reconciliation comes into play. A reconciliation engine is an isolated service that runs on a schedule, typically every few minutes or at the end of the day.

It reads the immutable Entries table, sums up all the debits and credits for every account since the beginning of time, and compares that calculated sum to the materialized balance in the Accounts table. If there is even a one-cent discrepancy, the system immediately fires a high-priority alert and freezes the affected account. You should also reconcile against external systems. For example, the total fiat balance in your ledger must perfectly match the balance reported by your external banking partners at the end of the settlement window. Bringing up reconciliation in your interview shows the panel that you think like a pragmatic engineering leader who anticipates failures and builds defensive systems.

Frequently asked questions

What is the most important metric in a ledger system design?

Correctness and strong consistency. Low latency is great, but it always takes a back seat to ensuring funds are never lost or double-spent. A slow transaction is annoying; a lost transaction is a regulatory disaster.

How do you guarantee exactly-once processing?

You require clients to send a unique idempotency key (like a UUID) with every single request. The ledger then verifies this key against a unique constraint in the database before it even attempts to process the transaction.

Should I use NoSQL for a financial ledger?

Generally, no. You will want to stick with relational or NewSQL databases. They provide the native ACID guarantees and Serializable isolation levels that are absolutely critical for handling financial data safely.

How do you handle a transaction that spans multiple currencies?

The core ledger should only ever process entries of the same currency. Build a separate exchange service to handle the conversion logic, which then submits a multi-leg transaction to the ledger containing the exact amounts in each currency based on the exchange rate at that exact millisecond.

Related comparisons

See AcePrompt in action

Watch how AcePrompt supports a real technical round - structured answers, tuned to your resume, in real time.

Nail your Coinbase system design interview with real-time AI guidance from AcePrompt.

Get started

See pricing →

Keep reading

Coinbase System Design Interview: Ledger Design Guide