Getting paid is nice! Most people enjoy seeing their bank account balance increase after a period of hard work. But have you ever wondered how your paycheck actually gets deposited into your bank account?
In this blog, we’ll explore how a basic payroll or direct deposit processing system ingests an incoming Nacha ACH file. This will give you a clearer picture of how your hard-earned money gets from your employer to you.
ACH Network Flow
Your paycheck most likely goes through Nacha’s ACH Network. This network handles most banking transactions, probably without you even realizing it. ACH stands for Automated Clearing House, and Nacha stands for the National Automated Clearing House Association. Nacha governs the ACH Network, setting rules that ensure financial transactions are sent and received smoothly and accurately between financial institutions.
The ACH file, as defined by Nacha, contains all the payment information, including the source, destination, and account details. I won’t delve into the specifics of the ACH file. For the sake of the blog, all you need to know is that it’s a 94-character fixed-width ASCII file. If you’re curious, though, you can find more information about ACH files on sites like this one.
Before your designated bank receives your paycheck, a few steps are needed to get the information ready for receiving. First, he payroll service provider creates the ACH file with bank account information for each employee and their salary. The payroll service provider sends the ACH file to the employer’s bank or other financial institution for processing. Upon receiving the payroll ACH file, the bank becomes the Originating Depository Financial Institution (ODFI).
The ODFI verifies the funds are available for payments and performs other verifications before sending the file to the Nacha network. The Nacha network then sends the payroll payments to the employees’ respective banks for deposit.
The next step is initiated when the bank receives the ACH file. The bank’s payroll application monitors for the ACH file to ingest the payroll information. In our example ACH file, the employer’s bank is the same as the employees’ bank. The employees’ banks, referred to as Receiving Depository Financial Institutions (RDFIs), are listed in the file. After the RDFI receives the file, employees will have access to their funds within a couple of business days.
A quick disclaimer: the example payroll application code written along with this article won’t cover most edge cases or rules, domain objects may lack some necessary properties, and the persistence structure needs to be more robust. It also won’t account for non-transaction days like weekends and holidays. Essentially, this is a basic “happy path” implementation, and further details are required to make it robust and production-ready.
Processing the Nacha ACH File
With that bit of housekeeping out of the way, let’s get started processing the ACH file to make sure the employees get their proper pay.
I’ve broken down the process into several steps, which I think will make this process easier to follow.
- Ingest the ACH file
- Validate file accuracy
- Flatten the data object structure
- Batch the payments for processing
- Daily payment processing
- Daily payment reconciliation
- Success
- Failure
Let’s dive into step one, ingesting the ACH file.
Step 1: Ingest the ACH File
This step will determine whether the file can be processed. Most failures at this stage will cause a hard stop, requiring manual intervention and preventing any payments in the file from being processed. While I won’t go into the details of handling issues related to file format or validity in this article, it’s crucial to record the failure and have a notification process in place so someone can begin the correction process.
To ensure the file is correct, I use the expected Java data types in the classes according to the ACH file specification. For example, if a field is expected to contain an amount, I use a BigDecimal
data type to represent that value. This approach slightly blurs the line between the first two steps because any violation of the property type will interrupt the file reading process.
Java property mapping with constraints:
I use a similar object structure as the file and the same Nacha ACH terminology to simplify file validation and issue reporting in the second step. The main goal is to populate the Java objects for file validation. To help you get a deeper understanding, I included an example of ACH file hierarchy and an example of Java ACH file representation below.
ACH File Hierarchy
Java ACH File Representation:
Data Specifications:
Data specifications are not immediately relevant to the purpose of this blog. However, the concept is related, and I believe it’s an important one to call out, so I’ve included a quick section here.
When working with data, there are various specifications that must be followed for records:
- An alphanumeric field must be left-justified and post-padded with spaces.
- A numeric field must be unsigned, right-justified and pre-padded with zeros.
- Certain fields, like those that denote codes, must have uppercase characters only.
- Trace numbers, which are assigned by the ODFI, must be ascending (although not necessarily consecutive) within a batch.
Source: nacha.org
Step 2: Validate Nacha ACH File Accuracy
In this step, we are not validating the actual value of the properties, just their existence. For example, we check that AchEntryDetailRecord.dfiAccountNumber
has a value, but we do not verify if it matches an internal account. To ensure the file is correct, I use validation constraint annotations on the Java objects representing the file.
@NotBlank(message = "DFI Account Number must be 8 characters") @Size(min = 1, max = 17) private String dfiAccountNumber;
The property value that is checked in this step is effectiveEntryDate
. Handling the AchBatchHeaderRecord.effectiveEntryDate
is crucial. This value determines which rules to apply.
First and foremost, the effectiveEntryDate
must be today’s date or later. If the effectiveEntryDate
is the next day or later, we will check during Step 4 (when we batch the payments for processing) to ensure it falls on a valid processing day. If the effectiveEntryDate
is today’s date, it is considered “Same Day ACH,” and specific conditions apply to its processing.
Same-Day Nacha ACH conditions:
While we won’t work with a same-day ACH file in this blog, readers should know which special conditions apply in case they come across it in the future. They are as follows:
- Must be less than or equal to $1,000,000
- Must not contain the SEC Code of IAT
- Must be received by the originating bank’s Same Day ACH processing deadline
In addition to validating basic constraints on the properties, we need to verify several details on the File and Batch records. According to the Nacha ACH specification, there are values at both the File and Batch levels that help verify the integrity of the file.
The AchFileControlRecord
includes batchCount
, entryAddendaCount
, entryHash
, totalDebitAmount
, and totalCreditAmount
. Similarly, the AchBatchControlRecord
includes entryAddendaCount
, entryHash
, totalDebitAmount
, and totalCreditAmount
. These values need to be counted, summed, or hashed to ensure integrity. If an expected value does not match the actual value, the file must fail, and a notification should be sent for manual intervention.
Step 3: Flatten the Structure
If the ACH file is well-formed and valid, we will flatten the data structure to make it easier to persist and assign to an internal batch for processing. The flattened PayrollPayment
class may appear to contain more data, but this is necessary for potential data reconciliation. Note that the data persistence in this example is not robust enough for production. A production solution will require data constraints for integrity and additional collections or tables for usability.
Step 4: Batch the Payments for Processing
The next step is to group all the payments into batches for processing. Batching transactions makes ACH transaction management more efficient by streamlining the process and reducing the risk of errors.
In this example, PaymentBatch
uses effectiveBatchDate
and originatingDFIIdentification
for batch grouping during scheduled processing. The effectiveBatchDate
is taken from AchBatchHeaderRecord.effectiveEntryDate
. Using originatingDFIIdentification
helps make validation more efficient by validating it once for the entire batch instead of for each payment. Depending on the volume and types of transactions, different batch groupings may be more suitable for processing.
As mentioned earlier, it is crucial to ensure that the AchBatchHeaderRecord.effectiveEntryDate
falls on a valid processing date. Invalid processing dates include weekends and federal holidays. The handling of invalid effective dates is predetermined by financial institutions and should not require manual intervention.
Maintaining the state of a batch is crucial for managing its lifecycle. In this step, batches should have a state value, such as “PENDING,” to indicate that additional payments can still be added. This state will also help determine which batches are ready for processing.
Similarly, state management is essential for the payment lifecycle. Payments may need to be stopped before they are processed and should be skipped when their assigned batch is processed.
Step 5: Daily Payment Processing
Now, let’s move on to the most anticipated yet challenging step: adding funds to the accounts.
To ensure there is sufficient time to process the day’s batched transactions, an internal policy must define the cut-off time for receiving and adding payments to the current day’s batch. For payroll applications, Same Day ACH is unlikely as payroll ACH files are typically received well in advance to ensure a smooth process. However, always cover all your bases and program as if Same Day ACH could happen. See the callout on Same Day ACH above for more details.
A scheduled job will be configured to run daily at a predefined time. For this basic example, I’m using Spring’s scheduling package, but a more robust and configurable solution may be necessary in real-world applications.
@Scheduled(cron = "0 0 19 * * ?") // Runs every day at 7 PM public void processScheduledPayments() { PaymentBatchState statePending = batchStateRepository.findByState("PENDING"); LocalDate today = LocalDate.now(); List<PaymentBatch> batchesToday = batchRepository.findByEffectiveBatchDateAndBatchState(today, statePending);
We have only briefly covered data validation. Before applying funds, it is essential to validate all payment and employee data to ensure accuracy and prevent fraud. Additionally, make sure that batch and payment states are updated, along with other reporting, so the code can resume correctly if a job needs to be restarted.
While the example code returns “true” when validating the originating financial institution, third-party services are available that provide a trusted source for this validation. In theory, the financial institution information should be correct coming from the Nacha network. However, verification is critical so that any incorrect or fraudulent modifications are detected.
The example code includes only a subset of the necessary validations to verify that an account should be funded. Refer to the list of ACH Return Codes to explore additional validation options.
Making Validation Robust:
Some validation data values:
- Receiving DFI Identification – Validity and eligibility of the receiving bank.
- Receiving Account Number – Correctness, format, and activity status of the account.
- Account Type – Correct account type (checking or savings).
- Name of Account Holder – Match the account holder’s name with the expected recipient.
- Transaction Amount – Ensure the payment amount is correct and within limits.
- Account Eligibility – Ensure the account can receive ACH payments.
- Receiving Account Status – Check if the account is active or closed.
- Payment Date – Ensure the payment is scheduled for the correct processing date.
- Duplicate Transaction Check – Prevent duplicate payments.
- Receiving Institution’s Risk Limits – Check for limits or thresholds at the RDFI.
- ACH Return Codes – Check for any previous return codes associated with the account.
Validation is crucial and deserves more attention than we can give it here. It’s essential to write extensive tests to ensure the validations work correctly. Confidence in the validation code should only come from proving it works as expected through multiple tests.
When updating payment states and account amounts, use transaction management to ensure atomic changes. The code responsible for updating account amounts may come from the bank’s internally provided code package or API, adding complexity to transaction management.
Once the payment transaction passes validation and the account is credited, mark the payment as PaymentState.PROCESSED
. The transaction described in the Nacha ACH file has now successfully updated the account’s amount in the database, completing its journey from the payroll service provider.
In the unfortunate event of a failed transaction, assign the respective Nacha ACH Return Code and record any additional information to assist in reconciliation.
Step 6: Daily Payment Reconciliation
With either a success or failure, the transaction outcome needs to be reconciled so internal records can be updated and payments can be easily audited. Proper reconciliation facilitates the auditing process, making it easier to track payments, identify discrepancies, and maintain financial integrity. Additionally, it helps detect and address any issues promptly, thereby enhancing the overall reliability and transparency of the payment system.
Successful Completion
Reconciliation will occur after a specified number of days, following a grace period during which no reversals, stops, or disputes have been received on the payment. This reconciliation will update internal records and does not need to be sent back to the ODFI.
Failed Completion – Returns
Failed transactions can be resolved either manually or through an automated process. Depending on the ACH Return Code, manual intervention may involve contacting the originating financial institution to address the issue. Otherwise, the receiving financial institution sends an ACH Return Entry to the ODFI for the failed payment.
An ACH Return Entry is created using the original Entry Detail of the failed payment, along with an Addendum record that includes the ACH Return Code and any additional information. Generally, the format of returned entries should match the original entries, with some exceptions. Refer to the “Nacha Operating Rules & Guidelines” for specific changes. Most changes involve updating the DFI values to switch the roles of the receiver and originator, ensuring counts are accurate, and using the appropriate Return Entry Code for the Transaction Code on the Entry Detail, new batch, and new trace numbers. In payroll scenarios, the received addenda are not sent back.
The addendum as part of a return has more defined fields. While a payment-related addendum has an AddendaTypeCode
of “05,” the return addendum is “99.”
Timely processing of failed transactions is crucial. Nacha’s Operating Rules require that failed payments be returned within two banking days. Most issues caught by the ODFI must be returned within this timeframe.
Final Thoughts
At a high level, getting paid seems straightforward. However, the details become complex quickly. Processing payroll through Nacha ACH files is essential for businesses and financial institutions, allowing smooth fund transfers between companies and employees.
The structured format of ACH files, along with rigorous validation processes and industry standards, ensures payments are efficient, secure, and error-free. Additionally, financial institutions must manage various scenarios such as consumer billing, accounts payable/receivable, microtransactions, and more.
Hopefully, this blog provides some insight into how your paycheck is processed and how a receiving financial institution handles the payroll ACH file.
To find the code on GitHub, follow this link. Head over to the Keyhole Dev Blog to discover more blogs like this one.