Case Study 1: CNB's Account Validation Stored Procedure
From Seven Implementations to One
Background
City National Bank had a problem that Kwame Asante described, with characteristic understatement, as "a rich diversity of interpretations of what constitutes a valid account." Seven different programs across three platforms validated accounts before processing transactions. Each had been written at a different time, by a different developer, with a different understanding of the business rules.
The CICS online teller transaction (written in 2008) checked account status, balance, and hold flags. The batch posting program (written in 2012) checked account status and balance but not hold flags — because in 2012, the hold flag feature didn't exist yet, and nobody went back to add it. The web service gateway (written in 2019) checked everything but also performed a dormancy check that no other program did. The ATM interface (2015) had its own version. The IVR system (2017) was a copy-paste from the ATM version with modifications. The mobile API (2020) was based on the web service version. And the internal audit batch (2022) had the most comprehensive validation — because the auditors kept finding gaps in the other six.
Lisa Park discovered the divergence during a regulatory exam prep. A regulator's test account was flagged as valid by the online teller but invalid by the batch posting program. The difference: the teller program accepted accounts in "PENDING_CLOSE" status for balance inquiries, while the batch program rejected them. Both behaviors were defensible. Neither was documented as the canonical rule.
"We spent three weeks reconciling the seven implementations," Lisa said. "We found 23 behavioral differences. Eleven of them had produced incorrect transaction processing in production. We estimated $340,000 in mis-posted transactions over two years."
The Decision
Kwame applied the scoring matrix:
| Dimension | Score | Justification |
|---|---|---|
| Access paths | 5 | Seven programs across CICS, batch, web, ATM, IVR, mobile, audit |
| Data intensity | 4 | Four tables: ACCOUNT_MASTER, ACCOUNT_RESTRICTIONS, CUSTOMER_PROFILE, ACCOUNT_HOLDS |
| Change frequency | 3 | Validation rules change 2-3 times per year with regulatory updates |
| Compute intensity | 4 | Light computation, heavy data reads |
| Security requirement | 4 | Regulatory requirement for consistent validation |
Total: 20. Clear stored procedure candidate.
Design Phase
Rob Nguyen led the technical design. The team made several deliberate architectural decisions:
Decision 1: Two procedures, not one. Rob split the validation into two procedures:
-
CNB.VALIDATE_ACCOUNT_QUICK— A lightweight check for balance inquiries and status lookups. Reads ACCOUNT_MASTER only. Returns status and balance. For the IVR and ATM paths where speed matters most. -
CNB.VALIDATE_ACCOUNT_FULL— The comprehensive validation for transactional operations. Reads all four tables. Checks dormancy, holds, restrictions, and customer status. Returns a detailed validation result structure.
"The temptation was to build one procedure that does everything," Rob explained. "But the ATM path doesn't need hold checks for a balance inquiry. Making the ATM call the full procedure adds 3ms of latency for zero business value."
Decision 2: Status code contract. The team defined a status code contract that all callers must follow:
Status Code 0: Account valid, proceed with transaction
Status Code 2: Account valid with warnings (dormancy, low balance)
Status Code 4: Account not found
Status Code 8: Account found but invalid for this operation
Status Code 12: System error (DB2 failure)
Status Code 16: Parameter error (null or malformed input)
Every caller checks the status code. The status message provides human-readable detail. This contract is documented in a copybook (ACTVLRSP) that all calling programs include.
Decision 3: No commit in the procedure. Both procedures are read-only validation. They execute SELECT statements only. COMMIT ON RETURN is NO. The calling program owns the unit of work.
Decision 4: Separate WLM environment for high-volume procedures. Rob created a dedicated WLM application environment (WLMACTVL) for the validation procedures, separate from the general-purpose stored procedure environment. This prevents a misbehaving procedure in the general environment from affecting account validation latency.
Implementation
The full validation procedure's CREATE PROCEDURE DDL:
CREATE PROCEDURE CNB.VALIDATE_ACCOUNT_FULL
(IN P_ACCOUNT_NUM CHAR(12),
IN P_OPERATION_TYPE CHAR(4),
OUT P_ACCOUNT_STATUS CHAR(2),
OUT P_CURRENT_BALANCE DECIMAL(15,2),
OUT P_AVAILABLE_BALANCE DECIMAL(15,2),
OUT P_CUSTOMER_ID CHAR(10),
OUT P_RESTRICTION_CODE CHAR(4),
OUT P_HOLD_AMOUNT DECIMAL(15,2),
OUT P_DORMANCY_DAYS INTEGER,
OUT P_STATUS_CODE INTEGER,
OUT P_STATUS_MSG VARCHAR(200))
LANGUAGE COBOL
EXTERNAL NAME ACTVLFSP
PARAMETER STYLE GENERAL
NOT DETERMINISTIC
READS SQL DATA
WLM ENVIRONMENT WLMACTVL
PROGRAM TYPE MAIN
COLLID CNBCOLL
COMMIT ON RETURN NO
ASUTIME LIMIT 2000;
Key design elements in the COBOL implementation:
Available balance calculation. The procedure calculates available balance as current balance minus hold amounts minus pending debits. This calculation previously existed in four different programs with three different interpretations of "pending debits."
3000-CALC-AVAILABLE-BALANCE.
MOVE LS-CURRENT-BALANCE TO WS-AVAILABLE
EXEC SQL
SELECT COALESCE(SUM(HOLD_AMOUNT), 0)
INTO :WS-TOTAL-HOLDS
FROM ACCOUNT_HOLDS
WHERE ACCOUNT_NUMBER = :LS-ACCOUNT-NUM
AND HOLD_STATUS = 'ACTIVE'
AND HOLD_EXPIRY_DATE > CURRENT DATE
END-EXEC
IF SQLCODE = 0 OR SQLCODE = +100
SUBTRACT WS-TOTAL-HOLDS FROM WS-AVAILABLE
END-IF
EXEC SQL
SELECT COALESCE(SUM(TRANSACTION_AMOUNT), 0)
INTO :WS-PENDING-DEBITS
FROM PENDING_TRANSACTIONS
WHERE ACCOUNT_NUMBER = :LS-ACCOUNT-NUM
AND TRANSACTION_TYPE = 'DEBIT'
AND PROCESSING_STATUS = 'PENDING'
END-EXEC
IF SQLCODE = 0 OR SQLCODE = +100
SUBTRACT WS-PENDING-DEBITS FROM WS-AVAILABLE
END-IF
MOVE WS-AVAILABLE TO LS-AVAILABLE-BALANCE
.
Operation-type-aware validation. The P_OPERATION_TYPE parameter controls which checks are performed:
4000-VALIDATE-FOR-OPERATION.
EVALUATE LS-OPERATION-TYPE
WHEN 'BALC'
PERFORM 4100-VALIDATE-BALANCE-INQUIRY
WHEN 'WDRL'
PERFORM 4200-VALIDATE-WITHDRAWAL
WHEN 'DPST'
PERFORM 4300-VALIDATE-DEPOSIT
WHEN 'XFER'
PERFORM 4400-VALIDATE-TRANSFER
WHEN OTHER
MOVE 16 TO LS-STATUS-CODE
STRING 'UNKNOWN OPERATION TYPE: '
LS-OPERATION-TYPE
DELIMITED BY SIZE
INTO LS-STATUS-MSG
END-EVALUATE
.
A withdrawal requires a positive available balance. A deposit only requires that the account is open. A transfer requires both positive available balance and no outbound restriction flags. By embedding these rules in the procedure, the seven calling programs no longer need to implement operation-specific logic.
Restriction checking. The procedure reads from the ACCOUNT_RESTRICTIONS table and applies rules based on the restriction code and operation type. This replaced the most divergent logic — the batch program ignored restrictions entirely, the web service treated "REVIEW" restrictions as hard blocks, and the CICS transaction prompted the teller for override authorization. The stored procedure now returns the restriction information and lets the caller decide how to handle the user interaction (prompt for override, auto-reject, etc.), while the business rule of what constitutes a restriction is centralized.
Migration
The migration took four months, not because the stored procedure was hard to build, but because seven teams had to modify seven programs:
Month 1: Stored procedure developed and deployed to QA. Test harness created with 156 test cases covering every known behavioral scenario.
Month 2: CICS online teller and web service gateway migrated. These were the highest-risk callers. Lisa personally reviewed every test result.
Month 3: Batch posting, ATM interface, and IVR migrated. The batch posting team discovered three additional business rules they had implemented locally that weren't in the stored procedure — all three were added after review.
Month 4: Mobile API and audit batch migrated. Internal audit actually expanded their test suite to validate that the stored procedure matched their (correct) implementation.
During the migration, both the old application-side logic and the new stored procedure ran in parallel for the CICS transaction. A comparison routine logged every case where the results differed. In the first week, there were 847 differences out of 1.2 million transactions. Lisa's team reviewed each one. Every difference was a case where the application-side logic was wrong and the stored procedure was correct.
Performance Results
Rob's benchmarks after the migration:
| Metric | Before (App-Side) | After (Stored Proc) | Change |
|---|---|---|---|
| CICS avg response time | 12.4 ms | 5.8 ms | -53% |
| Batch validation rate | 8,200/sec | 22,100/sec | +170% |
| Web service avg response | 34.2 ms | 18.7 ms | -45% |
| CPU per validation (DB2) | 0.06 ms | 0.09 ms | +50% |
| Total daily CPU (all callers) | 142 CPU-sec | 98 CPU-sec | -31% |
The total daily CPU actually decreased even though per-invocation CPU increased. Why? Because the stored procedure eliminated redundant SQL calls. The application-side logic in the batch program was making 4 separate SQL calls per account; the stored procedure consolidated them into optimized queries with a single parse and bind.
Lessons Learned
1. The copybook contract matters more than the procedure. The ACTVLRSP copybook that defines the response structure became the most important artifact. Every team includes it. When the response structure changes, the copybook change forces a recompile of every caller — which is exactly what you want.
2. Separate WLM environments for different SLAs. The quick-validation procedure (ATM, IVR) runs in a WLM environment with a 2ms velocity goal. The full-validation procedure runs in an environment with a 10ms velocity goal. Mixing them in one environment would force a single goal that compromises one path or the other.
3. Parallel running is non-negotiable. Without the parallel-run comparison during migration, the team would not have caught the 847 differences. Several were edge cases that no test suite would have covered — accounts in unusual states created by production incidents years earlier.
4. Don't centralize the UI decision. The stored procedure returns restriction codes and dormancy information. It does not return "show message X to the user." That's a presentation decision that varies by channel. The procedure provides data; the caller decides what to do with it.
5. Monitor from day one. Rob set up SMF 101 record monitoring for the stored procedure on the first day of production deployment. Within a week, he identified that the ATM interface was calling VALIDATE_ACCOUNT_FULL when it should have been calling VALIDATE_ACCOUNT_QUICK, adding 3ms of unnecessary latency to every ATM transaction. The ATM team fixed it in the next sprint.
Current State
Two years after deployment, the validation stored procedures are the most-called procedures in CNB's DB2 subsystem: 2.3 million calls per day across all seven callers. The procedure has been updated four times for regulatory changes. Each update was deployed once and immediately effective for all callers. The estimated savings from eliminating logic divergence: $500,000 per year in reduced mis-postings and audit findings.
Kwame Asante's assessment: "This is what stored procedures are for. Not because they're faster — although they are. Because we finally have one answer to the question 'is this account valid?'"
Discussion Questions
-
Rob split the validation into two procedures (quick and full) rather than using a flag parameter in a single procedure. What are the trade-offs of each approach? Under what circumstances would a single procedure with a mode parameter be preferable?
-
During parallel running, 847 out of 1.2 million transactions showed differences. All were cases where the old logic was wrong. What does this tell you about the quality of the existing test suites for those programs?
-
CNB chose PARAMETER STYLE GENERAL (no null handling) for the validation procedure. Under what circumstances would PARAMETER STYLE GENERAL WITH NULLS be necessary? What would change in the COBOL code?
-
The case study mentions that the batch posting team discovered three additional business rules during migration that weren't in the stored procedure. How should a team systematically identify all business rules embedded in existing programs before building the stored procedure?
-
CNB's validation procedures are read-only (READS SQL DATA). How would the design change if the procedure also needed to write an audit record for every validation? Consider the implications for COMMIT ON RETURN and the caller's unit of work.