Designing a Ledger System That Survives ACH Returns
If you are building anything that moves money, your ledger is the system that actually matters.
Designing a Ledger System That Survives ACH Returns
If you are building anything that moves money, your ledger is the system that actually matters.
Not the UI. Not the transfer service. Not even the ACH integration.
The ledger.
Because ACH transfers are asynchronous and reversible, your ledger cannot assume that money movement is final when a request is created. If you design it that way, it will eventually drift out of sync with reality.
I have seen this happen. It usually starts small. A return is not handled correctly. A retry double posts. A provisional credit is not reversed cleanly. Over time those small inconsistencies compound.
Let’s talk about how to design a ledger that does not fall apart when ACH behaves exactly the way it was designed to.
The First Rule: Never Mutate Balances Directly
One of the most common early mistakes is storing a single balance field on a user table and updating it in place.
For example:
User.balance = User.balance + 100
This works in demos. It fails in financial systems.
A proper ledger is append only. Every financial action creates entries. You do not modify history. You record new events that describe what happened. Instead of mutating a balance, you create ledger entries such as:
Debit external funding account 100 Credit user wallet 100
The current balance is derived from summing entries, not from overwriting a number. This gives you auditability, traceability and the ability to correct mistakes without rewriting history.
Separate Available Balance from Ledger Balance
ACH introduces timing gaps.
A user initiates a deposit today. Settlement may complete in two days. A return may occur three days later.
If you only track one balance, you will struggle to represent this accurately.
Most robust systems separate:
Ledger balance Available balance
Ledger balance reflects all posted entries including pending ones. Available balance reflects funds the user can actually spend.
When you provisionally credit a user after initiating an ACH debit, you may increase ledger balance immediately but conditionally increase available balance depending on risk rules.
If a return occurs, you post reversing entries and adjust available balance accordingly. This separation makes state transitions explicit rather than implicit.
Model Transfers as State Machines
An ACH transfer is not a single event. It is a lifecycle.
Initiated Submitted Pending settlement Settled Returned Reversed
Each of these states should be represented clearly in your system.
Your ledger entries should be tied to these transitions. For example, you may create pending ledger entries at initiation and convert them to settled entries upon confirmation.
The key idea is that your ledger should not guess. It should respond to explicit events.
This is where event driven architecture helps. Settlement files, processor webhooks and return notifications all drive state transitions.
Idempotency at Every Layer
If your transfer creation endpoint is idempotent but your ledger posting is not, you still have a problem.
Imagine a timeout occurs after posting ledger entries but before returning a response. The client retries. If you do not guard against duplicate posting at the persistence layer, you now have duplicate credits.
Idempotency should exist:
At the API layer At the transfer service layer At the ledger posting layer
Usually this means enforcing uniqueness constraints tied to a transfer identifier and ensuring ledger entries are only written once per state transition.
Financial correctness depends on this discipline.
Handling ACH Returns Cleanly
Returns are not edge cases. They are part of the ACH system. When a return arrives, your system must:
Identify the original transfer Create reversing ledger entries Update transfer state Adjust available balance if necessary
The reversal should not delete or edit the original ledger entries. It should create new entries that net out the financial impact. This preserves audit trails and keeps accounting clean.
One subtle challenge is handling cases where funds have already been spent. If a user deposits 100, spends 80 and then the deposit is returned, your system must handle negative balances or initiate recovery flows.
These scenarios should not surprise your architecture. They should be anticipated in the design.
Reconciliation Is a Daily Requirement
Even if your internal state machine is perfect, you still need reconciliation.
Processors generate daily reports. Sponsor banks provide settlement files. These should be matched against your internal transfer records.
Reconciliation jobs should:
Verify amounts Verify statuses Flag mismatches Alert on missing records
You cannot rely solely on webhooks. Files can be delayed. Notifications can fail. Networks are imperfect.
A well designed reconciliation pipeline is what gives you confidence that your ledger reflects external reality.
Concurrency and Race Conditions
Money movement systems are highly concurrent.
A user might initiate a withdrawal while a deposit is still pending. A return might arrive while funds are being spent. Multiple services might attempt to read and write balances simultaneously.
This is where transactional integrity matters. Ledger writes should occur within database transactions. Isolation levels should be chosen carefully. Optimistic locking or row level locking may be necessary depending on load patterns.
Financial systems are not tolerant of race conditions.
The Mindset Shift
Designing a ledger for ACH is not about building CRUD APIs. It is about accepting that money movement is delayed, reversible and externally influenced.
Your system should assume:
Events can arrive late Events can arrive twice External state can disagree temporarily Users can act while transfers are in flight
If your architecture handles those realities cleanly, you will sleep better.
If it assumes the happy path, you will eventually debug a balance mismatch at 2 am.
Fintech engineering becomes much more interesting once you realize the hardest problems are not about moving money. They are about modeling uncertainty correctly.
메타데이터
- post_id
- 5f89ef63569c
- slug
- designing-a-ledger-system-that-survives-ach-returns-5f89ef63569c
- url
- https://medium.com/@sneha.avula23/designing-a-ledger-system-that-survives-ach-returns-5f89ef63569c
- canonical_url
- https://medium.com/@sneha.avula23/designing-a-ledger-system-that-survives-ach-returns-5f89ef63569c
- author_url
- https://medium.com/@sneha.avula23
- status
- ok
- fetched_at
- 2026-06-26 03:39:16