← Back to list

Designing a Ledger System That Survives ACH Returns

If you are building anything that moves money, your ledger is the system that actually matters.

Sneha Avula · 2026-02-27 16:14 · 0 claps · 3.5 min read
#fintech #money-movement #plaid #finance-and-banking #banking-technology
Open on Medium ↗
Wiki topics: FIN · Fintech & Banking ECO · Economy · General

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