Case Study 1: Precision Matters -- The $0.01 Problem at National Savings Bank
Background
National Savings Bank (NSB) is a mid-sized financial institution headquartered in the American Midwest, managing approximately 4.2 million customer accounts with combined deposits exceeding $38 billion. Like many banks of its era, NSB runs its core banking platform on an IBM z/Series mainframe, with transaction processing written almost entirely in COBOL.
In fiscal year 2023, NSB processed an average of 12.7 million transactions per day---deposits, withdrawals, interest calculations, fee assessments, interbank transfers, and automated clearing house (ACH) operations. Each transaction involves at least one arithmetic operation on a monetary amount, and many involve several: computing interest, applying fees, calculating taxes, and updating running balances.
This case study examines what happened when a seemingly minor system modernization effort introduced a subtle arithmetic error that went undetected for three weeks, and how the principles of COBOL decimal arithmetic prevented a similar error in the legacy system for over 30 years.
The Modernization Initiative
In early 2023, NSB's technology leadership approved a project to modernize the bank's interest calculation engine. The existing COBOL batch program, INTCALC, had been in production since 1991 and performed daily interest accrual for all savings accounts. The program was reliable but slow---processing all 4.2 million accounts took approximately 4 hours in the nightly batch window.
The modernization team proposed rewriting the interest calculation logic as a Java microservice that would run on distributed Linux servers. The Java service would calculate interest for each account, then pass the results back to the mainframe for posting to the master account file.
The team consisted of five experienced Java developers and one COBOL consultant brought in to document the existing business logic. The project timeline was six months.
The COBOL Implementation
The original COBOL program calculated daily interest using straightforward packed-decimal arithmetic:
*================================================================*
* Daily interest accrual for savings accounts
* Interest = Balance * (Annual Rate / 365)
* All monetary fields use COMP-3 (packed decimal)
*================================================================*
01 WS-ACCOUNT-BALANCE PIC S9(11)V99 COMP-3.
01 WS-ANNUAL-RATE PIC SV9(8) COMP-3.
01 WS-DAILY-RATE PIC SV9(12) COMP-3.
01 WS-DAILY-INTEREST PIC S9(7)V99 COMP-3.
01 WS-ACCRUED-INTEREST PIC S9(9)V99 COMP-3.
COMPUTE WS-DAILY-RATE =
WS-ANNUAL-RATE / 365
COMPUTE WS-DAILY-INTEREST ROUNDED =
WS-ACCOUNT-BALANCE * WS-DAILY-RATE
ON SIZE ERROR
PERFORM 9100-LOG-OVERFLOW-ERROR
END-COMPUTE
ADD WS-DAILY-INTEREST TO WS-ACCRUED-INTEREST
ON SIZE ERROR
PERFORM 9100-LOG-OVERFLOW-ERROR
END-ADD
Several design decisions were critical:
-
COMP-3 for all monetary fields: Every dollar amount was stored in packed decimal format, ensuring exact decimal representation.
-
High-precision rate field: The daily rate used
PIC SV9(12), providing 12 decimal places for the intermediate rate calculation. This prevented premature truncation of the rate before it was applied to the balance. -
ROUNDED on the interest calculation: The daily interest was rounded to the nearest cent, not truncated.
-
ON SIZE ERROR on every operation: Any arithmetic overflow would be caught and logged immediately.
-
Separate accrual accumulator: Daily interest was accumulated in
WS-ACCRUED-INTERESTand posted to the account balance only at month-end, following standard banking practice.
The Java Implementation
The Java team implemented the equivalent calculation as follows:
// Daily interest calculation
double dailyRate = annualRate / 365.0;
double dailyInterest = accountBalance * dailyRate;
dailyInterest = Math.round(dailyInterest * 100.0) / 100.0;
accruedInterest += dailyInterest;
The code looked correct. The formula was identical to the COBOL version. The rounding logic used Math.round() to round to the nearest cent. The Java team ran unit tests with sample accounts and the results matched the COBOL output to the penny.
The system passed user acceptance testing with a set of 500 test accounts. It was promoted to production on March 1, 2023.
The Problem Emerges
On March 22, the bank's reconciliation team noticed a discrepancy. The total interest paid across all savings accounts for March (through the 21st) was $47,231.18 higher than the projection model predicted. The projection model was based on the same rates and balances but used high-precision decimal arithmetic in a financial modeling tool.
At first, the discrepancy was attributed to timing differences or rate changes. But by March 25, the cumulative overage had grown to $62,847.55. The reconciliation team escalated to IT.
Root Cause Analysis
The investigation took four days. The root cause was the interaction of three factors in the Java implementation:
Factor 1: Binary Floating-Point Representation
Java's double type uses IEEE 754 binary floating-point, which cannot exactly represent most decimal fractions. The annual rate of 3.75% was stored as:
COBOL COMP-3: 0.03750000 (exact)
Java double: 0.037499999999999999722444243843710864894092082977294921875
The difference is approximately 2.78 x 10^-19---seemingly negligible. But this tiny error propagated through every subsequent calculation.
Factor 2: Daily Rate Division
Dividing the annual rate by 365 compounded the representation error:
COBOL: 0.037500000000 / 365 = 0.000102739726 (exact to 12 places)
Java: 0.0375 / 365.0 = 0.00010273972602739725 (binary approximation)
Factor 3: Cumulative Rounding Bias
The critical issue was in the rounding step. Math.round(dailyInterest * 100.0) / 100.0 introduced two additional floating-point operations (multiply by 100, divide by 100), each with its own representation error. For certain account balances, the combined floating-point errors pushed the value just above or below the 0.5-cent threshold, causing incorrect rounding.
For example, an account with a balance of $45,287.33 at 3.75%:
True daily interest: $4.653767...
COBOL (COMP-3): $4.65 (truncated digit 3 < 5, rounds down)
Java (double): $4.66 (floating-point arithmetic produced
4.6500000000000004, which rounds up)
The Java calculation was wrong by one cent on this account---every single day.
Scale of the Error
Across 4.2 million accounts, approximately 340,000 accounts (8.1%) had balances and rates that produced these one-penny errors on any given day. The errors did not always go in the same direction, but due to the systematic nature of the floating-point representation, there was a net upward bias of approximately $2,400 per day.
Over the 21-day period before detection: $2,400 * 21 = approximately $50,400, closely matching the $47,231.18 observed discrepancy (the remaining difference was due to weekend and holiday processing variations).
The Regulatory Implications
NSB operates under several regulatory frameworks that mandate computational accuracy:
-
Truth in Savings Act (Regulation DD): Requires that interest calculations be accurate and that the method of calculation be disclosed to customers. Systematic overstatement of interest, even by pennies, constitutes a violation.
-
FDIC Examination Guidelines: Examiners verify that interest accrual systems produce accurate results. A systematic computational error would be flagged during examination.
-
SOX Compliance (Sarbanes-Oxley): Publicly traded banks must certify the accuracy of financial reporting. Interest expense is a material line item that must be accurate.
-
State Banking Regulations: Various state regulations impose additional accuracy requirements for consumer accounts.
The bank's compliance team estimated that the error, if left uncorrected for a full year, would have resulted in approximately $876,000 in excess interest payments and would likely have triggered regulatory action during the next examination.
The Resolution
NSB took the following corrective actions:
Immediate Fix (March 26)
The Java microservice was rolled back and the original COBOL batch program was reinstated for interest calculations. The COBOL program processed all 4.2 million accounts correctly that night, as it had for the previous 32 years.
Account Correction (March 27-April 5)
A special COBOL correction program was developed to: 1. Recalculate interest for all affected accounts from March 1-25 using the correct COBOL arithmetic 2. Compute the difference between the incorrect Java-calculated interest and the correct COBOL-calculated interest 3. Post adjusting entries to each affected account 4. Generate a regulatory disclosure report
The correction involved 340,000 accounts with adjustments ranging from -$0.01 to -$0.23, totaling $47,231.18 in reversals.
Long-Term Fix (April-June)
The Java implementation was rewritten using BigDecimal instead of double:
// Corrected daily interest calculation
BigDecimal dailyRate = annualRate.divide(
new BigDecimal("365"),
12, // 12 decimal places
RoundingMode.HALF_EVEN
);
BigDecimal dailyInterest = accountBalance.multiply(dailyRate)
.setScale(2, RoundingMode.HALF_EVEN);
accruedInterest = accruedInterest.add(dailyInterest);
The corrected Java implementation used:
- BigDecimal for exact decimal arithmetic (equivalent to COBOL's COMP-3)
- 12-decimal-place precision for intermediate calculations (matching the COBOL PIC SV9(12))
- RoundingMode.HALF_EVEN for banker's rounding
- Explicit scale setting for all results
After six weeks of parallel testing (running both COBOL and Java calculations and comparing results for every account every night), the Java service was re-deployed in June 2023.
Lessons Learned
Lesson 1: Decimal Arithmetic Is Not Optional for Financial Systems
The fundamental issue was not a bug in the Java code---the algorithm was correct. The issue was the choice of data type. Binary floating-point (double) is inherently unsuitable for financial calculations because it cannot exactly represent decimal fractions.
COBOL's COMP-3 (packed decimal) was designed specifically for this use case. It represents 0.10 as exactly 0.10, stores 3.75% as exactly 0.0375, and performs arithmetic with exact decimal precision. This is why COBOL has been the trusted language for financial processing for over 60 years.
Lesson 2: Small Errors Scale with Volume
A one-cent error seems trivial. But multiply it by 340,000 accounts per day, 365 days per year, and you have a material financial discrepancy. Financial systems must be correct to the penny on every transaction, because the aggregate impact of systematic errors is always significant.
Lesson 3: Testing Must Cover Precision Edge Cases
The Java implementation passed all 500 test accounts. But 500 accounts is a tiny sample of the 4.2 million in production. Precision errors are often triggered by specific combinations of balances and rates that may not appear in a small test set. Testing for financial arithmetic must include: - Boundary values (exact half-cent thresholds) - Large and small balances - Rates that produce repeating decimals - Extended-duration accumulation tests - Comparison against a known-correct reference (such as the existing COBOL system)
Lesson 4: Modernization Must Preserve Computational Guarantees
When modernizing a COBOL financial system, the replacement must provide identical computational guarantees. This means: - Exact decimal arithmetic (not floating-point) - Explicit rounding control (banker's rounding, not implementation-default) - Overflow detection (equivalent to ON SIZE ERROR) - Sufficient intermediate precision - Identical results to the penny for every account
Discussion Questions
-
Why does the binary representation of 0.0375 differ from the exact decimal value, and how does this difference propagate through subsequent calculations?
-
If the bank had used truncation instead of rounding in its COBOL program, would the error pattern have been different? Would it have been better or worse?
-
The bank initially tested with 500 accounts. How would you design a test suite to catch precision errors like this one?
-
What other programming languages offer exact decimal arithmetic similar to COBOL's COMP-3? How do they compare in terms of performance and ease of use?
-
The error was detected by the reconciliation team after 21 days. What automated monitoring could have detected it sooner?
Connection to Chapter Concepts
This case study directly illustrates several key concepts from Chapter 6:
-
COMP-3 packed decimal (Section: USAGE COMP-3): The original COBOL implementation used COMP-3 for all monetary fields, providing exact decimal representation.
-
ROUNDED phrase (Section: The ROUNDED Phrase): The COBOL program used ROUNDED to ensure interest was calculated to the nearest cent, not truncated.
-
ON SIZE ERROR (Section: ON SIZE ERROR and NOT ON SIZE ERROR): Every arithmetic operation included overflow protection.
-
Intermediate result precision (Section: Intermediate Results in COMPUTE): The daily rate was stored with 12 decimal places to prevent premature precision loss.
-
Banker's rounding (Section: Rounding Rules for Financial Applications): The corrected Java implementation adopted HALF_EVEN rounding, matching the financial industry standard.
The case demonstrates that these COBOL features are not academic exercises---they are engineering requirements for any system that handles money.