How to design a transaction reconciliation system for Plaid

Interviewing for a senior software engineering role at a fintech giant like Plaid means facing a system design round that plays by entirely different rules. In a typical consumer web app, eventual consistency works just fine. If someone's like count on a social media post lags by three seconds, nobody notices or cares. But in fintech, an incorrect account balance or a duplicated transaction creates a catastrophic failure. Real money is on the line, and the system design interview bar reflects this unforgiving reality.
Plaid sits at the center of the financial ecosystem, acting as the connective tissue between modern consumer applications and thousands of legacy financial institutions. Robust transaction reconciliation forms the absolute core of this infrastructure. Reconciliation is simply the process of ensuring your system's internal state perfectly matches the external state of the banks. Interviewers want to see if you can handle high throughput, out-of-order events, and strict transactional integrity without buckling under pressure. This guide breaks down exactly how to architect a fault-tolerant reconciliation engine.
Inside the Plaid System Design Interview: High-Throughput Fintech Expectations
When an interviewer asks you to design a transaction reconciliation engine, they're really testing how well you navigate the tension between massive scale and absolute correctness. Plaid processes tens of millions of transactions daily across thousands of bank integrations. Each of these banks brings its own API quirks, latency profiles, and webhook reliability issues to the table. Some banks fire off real-time webhooks, while others only drop nightly batch files via SFTP.
Expect heavy scrutiny on your database choices, concurrency management, and failure state handling. A senior candidate doesn't just draw generic boxes for web servers and databases on a whiteboard. They define the exact data model, pinpoint the specific isolation levels required, and build automated mechanisms for detecting anomalies. You have to approach this system with a deeply defensive mindset: assume every network call will eventually fail and every webhook will eventually duplicate.
The Core Challenge: Designing a Multi-Bank Transaction Reconciliation System
Transaction reconciliation boils down to a massive, complex matching problem. You maintain an internal ledger that records what you believe happened based on user actions, alongside external sources of truth like bank statements and webhooks telling you what actually settled. Your system has to consume these external events, match them against internal records, and immediately flag any discrepancies.
The gap between authorization and capture introduces major complexity here. When a user swipes a card, an authorization hold locks up the funds. Days later, the transaction officially captures and settles. Your reconciliation engine needs to track the lifecycle of a single transaction across these multiple states, accurately matching the final settlement event back to the original authorization.
System Architecture: Double-Entry Ledger Invariants
Building a highly reliable reconciliation engine requires a rock-solid foundation, which in fintech always means a double-entry ledger. In double-entry accounting, every single business transaction generates at least two entries: a debit to one account and a credit to another. The absolute, non-negotiable invariant here is that the sum of all debits and credits for any given transaction must always equal exactly zero.
Never propose a NoSQL database like Cassandra or MongoDB for the core ledger during this interview. While NoSQL databases scale horizontally with ease, they completely sacrifice the strong ACID guarantees required for financial data. Instead, reach for a robust relational database like PostgreSQL, or a distributed SQL database like CockroachDB or Google Spanner. You absolutely need Serializable isolation levels to prevent race conditions when updating account balances concurrently.
When the interviewer inevitably asks how you plan to handle a million transactions a second with a relational database, explain that the ledger itself doesn't need to absorb that peak load directly on a single node. You can horizontally shard the database by account ID. Since transactions typically only span a few accounts at a time, this strategy keeps distributed transactions to an absolute minimum.
The Precision Problem: Never Use Floating Point Numbers
Storing financial amounts using standard floating-point data types is a remarkably fast way to fail a fintech system design interview. Floating-point numbers are just approximations. They will eventually cause rounding errors that violate the strict zero-sum invariants of your ledger. If you lose even a fraction of a cent on every transaction at Plaid's scale, you'll quickly face massive, unexplainable accounting discrepancies.
Always explicitly define your data types. Propose storing all monetary amounts as integers that represent the smallest unit of currency, like cents for USD. A transaction of $10.50 gets stored as the integer 1050. As an alternative, you can use the NUMERIC or DECIMAL data types in PostgreSQL. These perform exact-precision mathematics, though they do incur a slight performance penalty compared to raw integers.

The Event-Sourced Storage Pattern
Candidates frequently make the mistake of treating transaction reconciliation as a basic CRUD operation. If a webhook arrives, they just update a status column on the transaction row from pending to settled. This represents a fatal flaw because it destroys historical context. What happens if the webhook payload was incorrect, or processed out of order? Building a resilient system requires an immutable event log.
Rather than updating rows in place, append incoming events directly to an Apache Kafka topic. When a bank webhook hits your API gateway, the gateway should perform minimal validation, assign a unique idempotency key based on the bank's transaction ID, and drop the raw payload into a Kafka topic named incoming-bank-events. Partition this Kafka topic by account ID. Doing so guarantees that a single consumer processes all events for a specific user's account strictly in order, wiping out an entire class of concurrency bugs.
Mitigating Chaos: Out-of-Order Webhooks and Race Conditions
Banks are notoriously bad at sending data in the correct order. In reality, you might easily receive a settlement webhook before you even get the initial authorization webhook. Your reconciliation engine can't just crash or drop data when this happens; it has to handle the chaos gracefully.
Design a robust state machine to manage your transactions. If a settlement event arrives for a transaction ID your system hasn't seen yet, don't discard it. Store it in a dedicated pending-matches table, or route it to a specialized Kafka topic for orphaned events. You can then run a background cron job or a delayed stream processor to periodically re-evaluate these orphaned events against newly created internal transactions.
To guarantee idempotency against duplicate webhooks, leverage a dedicated idempotency table in your PostgreSQL database, using the idempotency key as the primary key. Before processing any event, the worker attempts to insert the key into this table. If the insert violates the unique primary key constraint, you instantly know the event is a duplicate. At that point, you can safely acknowledge the webhook without mutating the ledger a second time.
Scaling the Pipeline: Batch Processing and Dead-Letter Queues
Real-time webhook reconciliation creates a great user experience, but end-of-day batch reconciliation remains absolutely mandatory for financial compliance. Banks send massive flat files at the end of the business day, often using archaic formats like BAI2 or MT940. Your system needs to ingest these massive files, parse them, and run a comprehensive diff against your entire internal ledger to ensure perfect alignment.
To handle this heavy lifting, propose a distributed batch processing framework like Apache Spark or AWS EMR. The daily batch job will load the day's internal transactions from a read replica or a data warehouse like Snowflake, load the parsed bank file from S3, and execute a distributed join on the transaction IDs and amounts. Since these datasets get incredibly large, explain how Spark handles shuffling and how you'd optimize the join by ensuring both datasets are partitioned by account ID.
Passing the Bar: Articulating Trade-Offs in Under 45 Minutes
You simply can't design every edge case of a global reconciliation engine in a standard 45-minute system design interview. The real secret to passing the Plaid senior engineering loop is actively driving the conversation toward the hardest problems. Don't wait passively for the interviewer to grill you about race conditions or database locks; bring them up proactively when you define your database schema and event pipeline.
Articulate your engineering trade-offs with clarity and confidence. If you pick PostgreSQL over a NoSQL solution, explain that you're intentionally trading write availability for strong consistency, which remains the only correct choice for a financial ledger. If you introduce Kafka, acknowledge the operational overhead it brings and explain exactly how you'll handle partition rebalancing alongside exactly-once processing semantics.
Ultimately, the interviewer just wants to know if they can trust you to build mission-critical systems that handle millions of dollars without losing a single cent. Focus relentlessly on data immutability, strict idempotency, and absolute database constraints. Do that, and you'll prove beyond a doubt that you have the engineering rigor required to pass the Plaid system design interview.
Frequently asked questions
Should I design a microservices architecture or a monolith for the reconciliation engine?
Start off by proposing a modular monolith for the core ledger. This ensures strict transactional boundaries and greatly simplifies your database commits. You should, however, decouple the ingestion of bank webhooks into separate microservices. Breaking it apart this way lets you scale the webhook ingestion pipeline entirely independently from the heavier, more complex ledger processing.
How do I handle banks that do not provide real-time webhooks for transactions?
Tell the interviewer you'll build a dedicated polling service. This service runs on a distributed scheduler, regularly fetching data via the bank's REST APIs or SFTP servers. It then normalizes those responses into the exact same event format used by your webhooks, feeding them seamlessly right into your Kafka pipeline.
What database isolation level is strictly required for a financial ledger?
You need to strongly advocate for Serializable isolation. Settling for lower levels like Read Committed or Repeatable Read opens the door to write skew or phantom reads when calculating account balances concurrently. If the interviewer pushes back on performance concerns, explain how sharding the database by account ID effectively mitigates contention.
How do I test a complex reconciliation system in an interview context?
Bring up chaos engineering and shadow testing. You can shadow live traffic by running the new reconciliation engine right alongside the legacy system to automatically compare their outputs. You should also discuss injecting synthetic failures—like duplicate webhooks, network drops, or malformed payloads—to prove your idempotency and error handling logic actually works.
Related comparisons
See AcePrompt in action
Watch how AcePrompt supports a real technical round - structured answers, tuned to your resume, in real time.
Practice your system design interviews with real-time AI feedback. Try AcePrompt today.
Get started