Demo · CardDemo

A mainframe credit card system, modernized one slice at a time.

CardDemo is a COBOL mainframe application that AWS publishes for testing modernization tools. We took three of its batch jobs (transaction posting, monthly interest and the daily transaction report) and two of its online screens (viewing and updating an account), rewrote them in Java, and proved the new versions produce exactly the same results as the original. Along the way, the tests turned up business rules that deserve a second look.

slices complete
3 of 6
posting transactions tested
309
interest charges tested
108
report lines compared
1,596
differences in output
0
screens compared
465
business findings
21

Status, October 2026: three batch slices and two online screens rewritten and proven against the original, on record files and on a PostgreSQL database. All of them run on AWS in the Parity Lab, where you can run them yourself, COBOL and Java side by side: the nightly batch, the account view and the account update.

1 · The system

What CardDemo is

CardDemo simulates the back office of a credit card company. AWS built it as a realistic mainframe application for trying out analysis and migration tools, and publishes it under the Apache 2.0 license (source on GitHub).

What it does

  • Online, during the day: staff sign on at green-screen terminals to view and update accounts, manage credit cards, look up and add transactions, take bill payments and request reports. Administrators manage users.
  • Batch, at night: a chain of jobs posts the day's card transactions to the accounts, calculates interest, and produces statements.

How big it is

  • 44 COBOL programs, about 22,900 lines
  • 17 online programs behind 21 terminal screens
  • 14 batch programs run by 47 JCL jobs and procedures
  • 13 VSAM and sequential data files, laid out by 62 copybooks
  • Optional modules that add Db2, IMS and MQ

2 · Today and the proposed design

How it is built, and where it is going

Everything runs on the mainframe today. The proposed design moves it to AWS one piece at a time, using the approach in our mainframe template.

Today, on the mainframe
Proposed, on AWS
Screens
3270 terminal screens21 BMS maps
Web front endRebuilt in a later slice
Online
CICS transactions17 COBOL programs
Java services in containers on Amazon ECSConverted with AWS Transform for mainframe, then proven
Batch
Nightly JCL job chain14 COBOL programs
Java jobs on AWS Batch, run in order by AWS Step FunctionsSlices 1 and 2 are the first of these
Data
VSAM indexed filesMainframe number formats
Amazon Aurora PostgreSQL, files in Amazon S3Moved after the batch jobs that use them
Sign-on
User file with plain-text passwordsPlus RACF
Amazon CognitoReplaced, not converted
Parity harness · each slice runs old and new on the same inputs, and every output is compared before the slice moves.

The nightly batch cycle

  1. Post transactions

    Job POSTTRAN checks each of the day's card transactions and posts it to the account, or rejects it.

  2. Calculate interest

    Job INTCALC charges monthly interest by account and spending category.

  3. Back up

    Job TRANBKP copies the transaction file and recreates it.

  4. Merge

    Job COMBTRAN sorts the backup and the interest charges back into one file.

  5. Statements

    Job CREASTMT writes account statements as text and HTML.

The slice plan

Batch first: a batch job reads files and writes files, so old and new can be compared record by record. The screens come once the data and test harness exist.

SliceWhat movesSizeStatus
1Daily posting (POSTTRAN)1 program, 619 linesComplete: proven identical
2Interest calculation (INTCALC)1 program, 552 linesComplete: proven identical
3Transaction report (TRANREPT)1 program, 545 linesComplete: proven identical
4Statements (CREASTMT)2 programs, 946 linesNext
5Online account, card and transaction screens9 programs, 8,794 linesPlanned
6Sign-on and user administration7 programs, 1,857 linesPlanned: replaced by Amazon Cognito

3 · Slice 1

What we did, step by step

The daily posting job reads the day's card transactions and, for each one, either posts it to the account or rejects it with a reason code: unknown card (100), unknown account (101), over the credit limit (102) or account expired (103).

  1. Inventory the system

    We read every program, job and copybook, mapped which programs read and write which files, and listed 12 risks with file and line references. POSTTRAN came out as the best first slice: one 619-line program, clear inputs and outputs, and sample data included.

  2. Run the original and keep its results

    No mainframe needed for this step: the unchanged COBOL program runs on Linux with GnuCOBOL, an open-source COBOL compiler. The harness loads CardDemo's sample data into indexed files the way VSAM holds them, pins the clock so every run is repeatable, runs the job, and saves every output file as the reference.

  3. Catch a silent error, in our own first run

    The first run looked fine: 257 posted, 43 rejected, a normal end. It was wrong. Mainframes store the sign of an amount inside its last digit (a G there means +7, a } means −0), and the compiler had not been told. It read $504.77 as $504.70 and a $919.00 refund as a $919.00 purchase. A newer compiler version misread it differently and rejected 299 of 300 transactions.

    With the sign convention set, both compiler versions agree byte for byte: 262 posted, 38 rejected. Every run is now also checked against an independent model of the posting rules written in Python, which fails on the wrong result. This is exactly the kind of error a conversion can introduce without anyone noticing.

  4. Fill the gaps in the test data

    The sample data only ever triggers one of the four rejection rules (over the limit). We added nine crafted transactions: an unknown card, a card pointing at a missing account, transactions dated on and after an account's expiry date, a purchase landing exactly on the credit limit, one cent over it, and a refund. All four rules and their boundaries are now tested.

  5. Write the Java version

    A Java 21 program that reads and writes the same record files as the COBOL. It follows the original's rules exactly, including details that are easy to miss: which rejection code wins when two rules fail, and leftover bytes the COBOL carries from one record into the next. Any change in behavior sits behind an explicit switch, off by default.

  6. Compare old and new

    Test dataTransactionsPostedRejectedResult
    CardDemo sample30026238Identical
    Edge cases30926544Identical

    Identical means every record in every output file (posted transactions, rejects, accounts and category balances) and every line the program prints. The only field set aside is the fraction of a second in the processing timestamp, which comes from the real clock.

  7. Report what the tests found

    The credit-limit check adds up the cycle's purchases and subtracts its refunds. Refunds are stored as negative amounts, so subtracting them adds them back: a refund lowers the balance but counts as extra spending against the limit. After a $50 refund, one test account had $2,070 counted against a $2,020 limit.

    It already affects the published sample data: 17 of the 38 rejections happen only because of it. Switching on the fix changes 19 decisions, not 17: the 17 transactions now post, and 2 later ones on the same accounts are rejected because the extra purchases use up the limit first.

    The Java version keeps the original behavior. Whether refunds should count against the limit is a business decision, so the fix stays off until it is approved, with the list of the 19 transactions it changes.

4 · Slice 2

Interest calculation

Job INTCALC charges interest each month. For every account it looks up an interest rate for each kind of balance (purchases, cash advances, credits and so on) and writes one interest charge per balance: the balance times the yearly rate, divided by 12. The total is added to the account.

  1. Run it after posting, the way the nightly batch does

    CardDemo's published balances are all zero, so on its own the job would only ever charge $0.00. We fed it the balances that slice 1's posting run produced, which is the order the jobs run in. It reads 100 balances across 50 accounts and writes 50 interest charges totaling $1,279.16.

  2. Reuse the harness

    Same compiler settings, same fixed clock, same checks. Two small stand-ins replace mainframe services: one passes the job's run date the way the mainframe does (it becomes part of every transaction ID), the other stands in for the routine that stops a job on an error. An independent Python model of the interest rules checks every charge, every account and every printed line.

  3. Confirm the defects the inventory predicted

    Reading the program during the inventory had flagged two defects before anything ran. The run confirmed both:

    • The last account never gets its interest. Each account is updated when the next one starts, and the job ends before the last one gets its turn. Its $18.77 charge is written, but its balance never includes it, and its monthly totals are never reset.
    • Interest is cut off, not rounded. $103.99 at 15% a year is $1.299875 for the month, charged as $1.29. 28 of the 50 charges are a cent short.

    It also showed smaller behaviors the business should know about: a zero balance still produces a $0.00 charge, a credit balance earns negative interest, and fees are never calculated (that part of the program is empty).

  4. Test the cases the sample never reaches

    Every account in the sample uses the default rates. We added six accounts: one with its own rates (15% on purchases, 25% on cash advances, nothing on credits), one at 0%, a credit balance, a rate table missing a category, an unknown rate group, and the rounding case above. The expected result for each was worked out by hand before the run.

  5. Compare old and new

    Test dataBalancesInterest chargesTotal interestResult
    CardDemo, after posting10050$1,279.16Identical
    Edge cases11058$1,323.78Identical

    Identical again means every interest charge, every account record and every line the program prints.

  6. Show exactly what each fix would change

    Both fixes are written and switched off. Run against the same data, the last-account fix changes one account (balance up $18.77, monthly totals reset), and rounding raises 28 charges by one cent, $0.28 in total.

    As with refunds in slice 1, the Java version keeps the original behavior. Correcting the last account is almost certainly wanted; rounding changes what customers are charged, so it needs a business decision. Either way, the list of affected accounts comes with it.

5 · Slice 3

Daily transaction report

Job TRANREPT sorts the day's transactions by card and prints them: one line per transaction with its type and category, a total after each card, a total at the foot of each page and a grand total at the end. It is also the report users request from the online Reports screen.

  1. Run it last, on what posting and interest wrote

    As in the nightly batch, the posted transactions and the interest charges are merged into the transaction file first, then the report runs on them: 312 transactions on 50 cards. The job's own dates (January to early July 2022) are older than our pinned run date, so every transaction would be filtered out; we report on the run date instead.

  2. Compare a printed report, line by line

    The output is a report, not data records, so every line is compared in full, all 133 characters including trailing blanks, along with everything the program prints. The job's sort step, which runs on the mainframe's sort utility, is reproduced by a small script; the report program itself runs unchanged. An independent model of the report checks every line.

  3. Report what the tests found

    This program had the most to show:

    • The grand total is wrong. At the end of the file the program adds the last transaction a second time: $79,254.29 instead of $79,233.86. The difference, $20.43, is exactly the last transaction.
    • The last card's total is never printed. 49 card totals for 50 cards.
    • Pages run on. Pages are meant to be 20 lines, but 8 of the 18 pages run to 40 or 42 lines.
    • "Account Total" is really a card total. An account with two cards gets two totals.
    • The report can stop early and still report success. The date range is set in two places, the sort step and a date file. If they disagree, the report stops at the first transaction outside the range, with no page total and no grand total, and the job still ends normally. The online Reports screen writes the same dates into both; running the job directly does not.
  4. Test the cases the data never reaches

    Six extra transactions on two extra cards: a second card for an existing account, the largest amount the report can print ($123,456,789.01), a negative adjustment, a zero amount (it prints as blanks), and a transaction from the day before, which the sort step filters out. A third run widens the sort step's dates so that one transaction gets through to the program, to show the early stop.

  5. Compare old and new

    Test dataTransactionsReport linesResult
    Nightly data312519Identical
    Edge cases317540Identical
    Dates out of step318537, no totalsIdentical: Java stops at the same line

    Two compiler versions, GnuCOBOL 3.1.2 and 3.2, produce byte-identical reports.

  6. Show exactly what each fix would change

    Three fixes are written and switched off. Counting the last transaction once and printing the last card's total gives a grand total of $79,233.86, exactly posted plus interest. Breaking pages at 20 lines gives 25 even pages with the same totals. Skipping a transaction outside the dates, instead of stopping, brings the totals back.

    Totals per card instead of per account stay as they are: changing that changes the report's layout and sort order, so it is a business decision. You can switch the report fixes on in the Parity Lab and see the end of both reports side by side.

6 · The data

Moving the data into a database

Up to this point the Java versions read and wrote the same record files as the mainframe. That proves the logic, but a modern system keeps its data in a database. This step moves CardDemo's data into PostgreSQL and runs the three jobs on it, with the same checks.

  1. Give the data real types

    Every record layout became a typed data model: money as exact decimals, dates as dates, ids as numbers. One piece of code converts between the model and the mainframe layouts. All 1,097 records in the 16 files involved, CardDemo's data, the daily input, the reference outputs and the edge cases, convert and come back byte for byte.

  2. Migrate, and prove nothing was lost

    The migration loads CardDemo's six data files into PostgreSQL and exports them again: all 226 records come back byte for byte. A record whose fields do not fit their types is refused and listed rather than quietly changed. On CardDemo's data, none is.

  3. Report what the data shows

    The migration also checks the data itself. In all 50 sample accounts the interest-rate group (A000000000) sits in the ZIP code field and the group field is empty. That is why every account in slice 2 used the default rates. It is a question for whoever owns the data, so the migration reports it instead of fixing it.

  4. Run the jobs on the database

    The three jobs were rewritten on the data model, with storage behind one interface, so the same code runs on the record files or on PostgreSQL. After each job the database is exported back to the record layouts and compared with the COBOL exactly as before.

    CheckRunsResult
    Each job and test case, on record files through the new data model7Identical
    Each job and test case, on PostgreSQL7Identical
    The whole nightly chain inside the database3 jobsIdentical
  5. Make a failed run harmless

    On the mainframe, a posting run that stops part-way leaves the files half updated; the inventory flagged this before anything ran. In the database each job saves its changes in one transaction. Two tests prove it: a run that stops on a duplicate transaction id, and one where the database refuses the 262nd row after 261 have been written. Both leave the database exactly as it was.

  6. Drop what the database makes unnecessary

    On the mainframe a separate job merges the interest charges back into the transaction file every night. In the database each job adds its rows and keeps the history, so that step is gone. The day's incoming transactions still arrive as a file, as they would from a card network, and the report is still a printed report.

  7. Run it on AWS

    In the Parity Lab you can choose PostgreSQL for the Java side. It runs on Amazon Aurora Serverless, which pauses after five idle minutes and has no compute charge while paused; the first run after a pause waits about 15 seconds while it wakes. Each run gets its own schema, removed when the run ends.

    All 19 checks above, the 17 comparisons and the 2 failure tests, pass on two machines with PostgreSQL 16. In the Parity Lab the same comparisons pass on Amazon Aurora PostgreSQL 16.

7 · The first online screen

From green screen to web page

CardDemo's users work on 3270 "green screens" run by CICS, the mainframe's online transaction system. This step takes the View Account screen (program COACTVWC) and gives it a Java service and a web page, with the original screen as the reference.

  1. Run the original screen without a mainframe

    There is no CICS outside the mainframe, so each of the program's 15 CICS commands is replaced with a call to a small stand-in: reading a file, sending the screen, receiving what was typed, returning with its saved data. The rest of the program runs unchanged under GnuCOBOL. A driver plays the terminal, one key press per task, as CICS runs it, and draws each screen from the program's own screen definition: positions, labels, colors and the cursor.

  2. Record 76 screens

    Eleven scenarios: every one of the 50 sample accounts; blank, non-numeric, all-zero, short and unknown account numbers; other keys; arriving from the main menu and leaving with PF3. Four test records cover what the sample data never reaches: a card whose account record is missing, an account whose customer is missing, extreme amounts, and an account with two cards.

  3. Build the service, compare screen by screen

    The Java service looks up the account, its card and its customer on the same data model as the batch jobs, from the record files or from PostgreSQL, and returns JSON for the web page. For the comparison its answer is laid onto the original screen layout, and every field, every color and the cursor are compared with what the COBOL sent.

    RunScreensResult
    Java service on the record files76Identical, except one screen (below)
    Java service on PostgreSQL76Identical, except the same screen

    The one exception is the card whose account record is missing. The original fills five amount fields from storage the read never filled, and on a mainframe that storage could hold anything. The service leaves them empty, and the comparison lists them as an expected difference.

  4. What the tests found

    • An account with more than one card cannot be opened. The card index allows several cards per account, but the program treats the "duplicate key" answer as a file error. The sample data has one card per account, so this never shows.
    • A card whose account record is missing still shows its customer, under an error message, with account amounts from leftover storage. The check meant to stop this tests for a message that is never set.
    • Messages show internal codes such as Resp:000000013 Reas:0000 and are cut off at 75 characters.
    • Amounts of a billion or more lose their first digit on screen: a credit limit of 9,999,999,999.99 shows as 999,999,999.99.
    • The full Social Security number, government id and bank account are shown to anyone who can open the screen.

    Each one is a switch in the Java service, off by default: with a switch on, the comparison shows exactly which fields change and why.

  5. Try it

    The account screen in the Parity Lab runs both on AWS for any account number you type: the COBOL's 3270 screen on the left, the web page on the right, and the comparison underneath.

8 · The first screen that writes

Updating an account, and proving what gets saved

The Update Account screen (program COACTUPC) is CardDemo's largest program: 4,236 lines that check about forty fields, ask for confirmation with F5, and rewrite the account and its customer together. Here parity means two things: the same screens, and the same bytes saved.

  1. Add the writes to the CICS stand-in

    The view screen's stand-ins gain three commands: READ UPDATE, REWRITE, and SYNCPOINT with ROLLBACK. The rewrite stand-in keeps a before-image of each record it changes, so a rollback really puts the old record back. The program's date check calls an IBM Language Environment service, CEEDAYS, which gets a small stand-in of its own. The terminal stand-in now also refuses to type into protected fields, as a real 3270 does. After every scenario, the files are written out again and every changed record is kept.

  2. Record 389 screens, and every write

    Twenty scenarios: field checks one rule at a time (about 70 wrong values, each answered by its own message), dates and leap years, money in every form the program accepts, saving, clearing optional fields, other keys, and all 50 sample accounts. Four of them change the data behind the user's back: another user changes the record or deletes the account or the customer between Enter and F5, or the customer rewrite is made to fail.

  3. Carry the rules over, then compare

    The Java service takes the rules over one by one, in the program's order and with its exact messages. The way the screen reads money fields (5000, -5,000.50, $1,000, 5000CR) follows the compiler's own routine, and was checked against it on 40,000 generated inputs with no disagreement. Saving is one transaction: the database locks both rows, checks nobody changed them, and writes both or neither.

    RunScreensRecords savedResult
    Java service on the record files3898Identical, except one case (below)
    Java service on PostgreSQL3898Identical, except the same case

    The exception is a day typed as one digit. The original saves it as 2014-11-5 , which no date column can hold, so the Java side refuses the save and says why, and the comparison lists it as an expected difference. Changing a single rule in the Java (a FICO limit of 851 instead of 850) makes the comparison fail.

  4. What the tests found

    • The account group is saved in the wrong place. The record the program writes has no zip-code field, so the group lands where the zip code belongs and the real group field is blanked. A new group never shows again, and the value that was in the zip-code position is overwritten.
    • A one-digit day is accepted and saved as 2014-11-5 , a date other programs cannot read. A one-digit month is rejected.
    • Zip codes that start with 0 can never be saved. For Connecticut, Massachusetts, Maine, New Hampshire, New Jersey, Puerto Rico, Rhode Island and Vermont, the program's state table holds the second and third digits of the zip code instead of the first two. A Hartford address, 06103, is rejected; 60103 is accepted.
    • Cleared fields are written as binary zeros. PostgreSQL text cannot hold them, so the database version needs a decision; here it keeps a stand-in character and gives the zeros back on export.
    • When the customer record cannot be locked, the screen says "Changes committed to database" next to the error, though nothing was written.
    • Pressing Enter without changing anything counts as a change when a field is blank, so the checks run on stored data, and a save then writes the blank as binary zeros.
    • The second and third parts of the SSN are checked only when the first part is a number: an IF statement is closed by a full stop, not by its END-IF.
    • None of the 50 sample accounts passes the program's own checks as stored: zip codes that do not match the state, FICO scores under 300, area codes not in the program's list. Rules this strict, on data this loose, have to be settled before a migration, not after.

    Seven of these are switches in the Java service, off by default. With a switch on, the comparison shows which screens and bytes change, and why.

  5. Try it

    The account update in the Parity Lab runs both on AWS on fresh copies of the data: pick an account and some changes, or one of the cases above. You see each step's 3270 screen, the Java service's answer, and the records both sides saved, byte by byte.

9 · Limits

What this does not prove yet

  • GnuCOBOL is not the mainframe. The reference results come from GnuCOBOL. A run on z/OS or AWS Mainframe Modernization would close that gap.
  • ASCII sample data. CardDemo also ships the data in mainframe encoding (EBCDIC), where sort order differs. That check is still to do.
  • Some failure paths are untested: in posting, an account update that fails part-way and a duplicate transaction ID; in interest, a missing account, card or default rate, and an account with two cards. Each stops the original job, and testing it needs a broken file rather than different data.
  • The sort steps are stand-ins. The jobs' mainframe sort steps are reproduced by small scripts. The report's sort does not fix the order of transactions within a card, so on the mainframe that order may vary; we keep them in transaction order.
  • The CICS stand-ins are not CICS. They follow the documented behavior of each command the two account screens use, and their assumptions are listed in the engineering record. Two screens are covered so far; CardDemo has 17 online programs. Record locking is tested by changing the data between key presses, not by two users at once.
  • Some compiler behavior differs. Where a field of digits holds a blank (a month typed as one digit, say), GnuCOBOL and IBM's compiler may decide differently. The reference is GnuCOBOL; the engineering record lists the cases.
  • Not yet a production setup. All three slices run on AWS in the Parity Lab, on AWS Lambda and AWS Step Functions, with the Java side on record files or on Amazon Aurora. A production setup would run them on a schedule against the live database, and the database versions of the jobs, which read whole tables into memory, would page through them at production volumes.

Next

More screens, and slice 4, statements. The card list, card view and card update screens use the same stand-ins and data. The statement job writes text and HTML statements per account, on the same data and harness.

Every step, tool and check behind this page is written up in the engineering record, including how AI assistance was used.

Back to all demos

Want this kind of evidence for your system?

A two-to-three-week assessment maps your system, finds the risks and gives you a fixed price for the first slice.

Book an assessment call