Sunday, August 23, 2026

COBOL - The Verbose Ancient Programming Language for Business Applications



Introduction

Do you remember COBOL, this ancient dinosaur which had been the number One language for a very long time? Ok, Fortran as a scientific programming language was also widely used.


Here’s a complete, portable COBOL program (works nicely with GnuCOBOL) that lets the user pick a calculation (VAT from net, VAT from gross, compound interest, simple interest), then prompts for the required values and prints the results.


       IDENTIFICATION DIVISION.

       PROGRAM-ID. FINANCE-CALC.

       AUTHOR. YOU.


       ENVIRONMENT DIVISION.

       CONFIGURATION SECTION.

       SPECIAL-NAMES.

           *> If you prefer comma decimals (e.g., 19,5), uncomment next line:

           *> DECIMAL-POINT IS COMMA.


       DATA DIVISION.

       WORKING-STORAGE SECTION.

       01  WS-MENU-CHOICE              PIC 9 VALUE 0.

       01  WS-CONTINUE                 PIC X VALUE 'Y'.


       *> Common fields

       01  WS-RATE-PCT                 PIC 9(5)V9(4) VALUE 0.     *> e.g., 19.0 for 19%

       01  WS-RATE                     PIC 9(2)V9(8) VALUE 0.     *> decimal, e.g., 0.19

       01  WS-NET                      PIC 9(12)V9(4) VALUE 0.

       01  WS-GROSS                    PIC 9(12)V9(4) VALUE 0.

       01  WS-VAT                      PIC 9(12)V9(4) VALUE 0.


       *> Compound/Simple interest fields

       01  WS-PRINCIPAL                PIC 9(12)V9(4) VALUE 0.

       01  WS-YEARS                    PIC 9(4)V9(4)  VALUE 0.

       01  WS-COMPOUNDS-PER-YEAR       PIC 9(3)       VALUE 0.

       01  WS-PERIODS-TOTAL            PIC 9(7)       VALUE 0.

       01  WS-PERIOD                   PIC 9(7)       VALUE 0.

       01  WS-PERIOD-RATE              PIC 9(2)V9(8)  VALUE 0.

       01  WS-AMOUNT                   PIC 9(12)V9(4) VALUE 0.

       01  WS-INTEREST                 PIC 9(12)V9(4) VALUE 0.


       *> Display helpers (2 decimals for money)

       01  DS-NET                      PIC Z(12)9.99.

       01  DS-GROSS                    PIC Z(12)9.99.

       01  DS-VAT                      PIC Z(12)9.99.

       01  DS-AMOUNT                   PIC Z(12)9.99.

       01  DS-INTEREST                 PIC Z(12)9.99.

       01  DS-RATE-PCT                 PIC Z(5)9.9999.


       PROCEDURE DIVISION.

       MAIN-LOOP.

           PERFORM UNTIL WS-CONTINUE NOT = 'Y'

              PERFORM SHOW-MENU

              PERFORM PROCESS-CHOICE

              DISPLAY "Do another calculation? (Y/N): " WITH NO ADVANCING

              ACCEPT WS-CONTINUE

              MOVE FUNCTION UPPER-CASE(WS-CONTINUE) TO WS-CONTINUE

           END-PERFORM

           DISPLAY "Bye!" 

           STOP RUN.


       SHOW-MENU.

           DISPLAY "==============================================".

           DISPLAY " Finance Calculator".

           DISPLAY " 1) VAT from NET (NET -> VAT + GROSS)".

           DISPLAY " 2) VAT from GROSS (GROSS -> VAT + NET)".

           DISPLAY " 3) Compound Interest (principal grows over time)".

           DISPLAY " 4) Simple Interest".

           DISPLAY " 0) Exit".

           DISPLAY "==============================================".

           DISPLAY "Your choice (0-4): " WITH NO ADVANCING

           ACCEPT WS-MENU-CHOICE.


       PROCESS-CHOICE.

           EVALUATE WS-MENU-CHOICE

             WHEN 1

                PERFORM VAT-FROM-NET

             WHEN 2

                PERFORM VAT-FROM-GROSS

             WHEN 3

                PERFORM COMPOUND-INTEREST

             WHEN 4

                PERFORM SIMPLE-INTEREST

             WHEN 0

                MOVE 'N' TO WS-CONTINUE

             WHEN OTHER

                DISPLAY "Invalid choice."

           END-EVALUATE.


       ASK-RATE.

           DISPLAY "Enter rate in percent (e.g., 19 or 19.5): " WITH NO ADVANCING

           ACCEPT WS-RATE-PCT

           COMPUTE WS-RATE ROUNDED = WS-RATE-PCT / 100.


       VAT-FROM-NET.

           DISPLAY "Enter NET amount: " WITH NO ADVANCING

           ACCEPT WS-NET

           PERFORM ASK-RATE

           COMPUTE WS-VAT   ROUNDED = WS-NET * WS-RATE

           COMPUTE WS-GROSS ROUNDED = WS-NET + WS-VAT

           MOVE WS-NET   TO DS-NET

           MOVE WS-RATE-PCT TO DS-RATE-PCT

           MOVE WS-VAT   TO DS-VAT

           MOVE WS-GROSS TO DS-GROSS

           DISPLAY "NET:   " DS-NET

           DISPLAY "Rate:  " DS-RATE-PCT " %"

           DISPLAY "VAT:   " DS-VAT

           DISPLAY "GROSS: " DS-GROSS.


       VAT-FROM-GROSS.

           DISPLAY "Enter GROSS amount: " WITH NO ADVANCING

           ACCEPT WS-GROSS

           PERFORM ASK-RATE

           *> VAT part = GROSS * (rate / (1 + rate)), NET = GROSS - VAT

           COMPUTE WS-VAT   ROUNDED = WS-GROSS * (WS-RATE / (1 + WS-RATE))

           COMPUTE WS-NET   ROUNDED = WS-GROSS - WS-VAT

           MOVE WS-GROSS TO DS-GROSS

           MOVE WS-RATE-PCT TO DS-RATE-PCT

           MOVE WS-VAT   TO DS-VAT

           MOVE WS-NET   TO DS-NET

           DISPLAY "GROSS: " DS-GROSS

           DISPLAY "Rate:  " DS-RATE-PCT " %"

           DISPLAY "VAT:   " DS-VAT

           DISPLAY "NET:   " DS-NET.


       COMPOUND-INTEREST.

           DISPLAY "Enter principal (start amount): " WITH NO ADVANCING

           ACCEPT WS-PRINCIPAL

           PERFORM ASK-RATE

           DISPLAY "Years (e.g., 5 or 5.5): " WITH NO ADVANCING

           ACCEPT WS-YEARS

           DISPLAY "Compounds per year (e.g., 1, 4, 12): " WITH NO ADVANCING

           ACCEPT WS-COMPOUNDS-PER-YEAR


           IF WS-COMPOUNDS-PER-YEAR = 0

              DISPLAY "Compounds per year must be >= 1. Using 1."

              MOVE 1 TO WS-COMPOUNDS-PER-YEAR

           END-IF


           COMPUTE WS-PERIODS-TOTAL ROUNDED =

                   FUNCTION INTEGER(WS-YEARS * WS-COMPOUNDS-PER-YEAR)

           COMPUTE WS-PERIOD-RATE ROUNDED = WS-RATE / WS-COMPOUNDS-PER-YEAR

           MOVE WS-PRINCIPAL TO WS-AMOUNT


           *> Iterative multiplication to avoid EXP/LOG dependencies

           MOVE 0 TO WS-PERIOD

           PERFORM UNTIL WS-PERIOD >= WS-PERIODS-TOTAL

              COMPUTE WS-AMOUNT ROUNDED = WS-AMOUNT * (1 + WS-PERIOD-RATE)

              ADD 1 TO WS-PERIOD

           END-PERFORM


           COMPUTE WS-INTEREST ROUNDED = WS-AMOUNT - WS-PRINCIPAL


           MOVE WS-AMOUNT   TO DS-AMOUNT

           MOVE WS-INTEREST TO DS-INTEREST

           MOVE WS-RATE-PCT TO DS-RATE-PCT


           DISPLAY "Principal:      " WS-PRINCIPAL

           DISPLAY "Rate (annual):  " DS-RATE-PCT " %"

           DISPLAY "Years:          " WS-YEARS

           DISPLAY "Compounds/year: " WS-COMPOUNDS-PER-YEAR

           DISPLAY "Final amount:   " DS-AMOUNT

           DISPLAY "Total interest: " DS-INTEREST.


       SIMPLE-INTEREST.

           DISPLAY "Enter principal (start amount): " WITH NO ADVANCING

           ACCEPT WS-PRINCIPAL

           PERFORM ASK-RATE

           DISPLAY "Years (e.g., 5 or 5.5): " WITH NO ADVANCING

           ACCEPT WS-YEARS


           COMPUTE WS-INTEREST ROUNDED = WS-PRINCIPAL * WS-RATE * WS-YEARS

           COMPUTE WS-AMOUNT   ROUNDED = WS-PRINCIPAL + WS-INTEREST


           MOVE WS-INTEREST TO DS-INTEREST

           MOVE WS-AMOUNT   TO DS-AMOUNT

           MOVE WS-RATE-PCT TO DS-RATE-PCT


           DISPLAY "Principal:      " WS-PRINCIPAL

           DISPLAY "Rate (annual):  " DS-RATE-PCT " %"

           DISPLAY "Years:          " WS-YEARS

           DISPLAY "Final amount:   " DS-AMOUNT

           DISPLAY "Interest:       " DS-INTEREST.


A fun thought experiment! 

If “COBOL” were invented today for its original niche—business systems, finance, ledgers, batch + online processing—it would probably look like a memory-safe, strongly typed, declarative-first language with native money/time types, effect safety, and frictionless data/DB/stream integration. Think of the clarity of classic COBOL, the ergonomics of TypeScript/Kotlin, the decimal rigor of SQL, and the reliability features of Rust.


Here’s what such a “Modern COBOL” (let’s call it Cobol-Next) would likely include:


What it would prioritize

  • Human-readable but compact syntax. Sentence-like keywords, but no shouting case or needless verbosity.
  • First-class financial primitives. money<EUR>, rate%, decimal(38,4), rounding(mode=Bankers, scale=2), fx(EUR->USD, at=ECB).
  • Time & calendars. date, timestamp, period, business_day(calendar=TARGET2), holiday calendars, day_count(Actual_360, 30E_360).
  • Schema-aware data. Native table/record types; transparent I/O for CSV/Parquet/JSON/Avro; schema evolution baked in.
  • SQL & streams built in. select/group by inline, plus from stream payments window 15m tumble ....
  • Deterministic decimal math. No binary float surprises; explicit rounding; overflow checks by default.
  • Effect & error safety. Result<T,E>, try, defer, capability-scoped I/O (@effects(db, net)), transactional blocks.
  • Concurrency for business. Async I/O, actors for services, sagas/workflows for distributed transactions.
  • Compliance hooks. Audit logs, immutability options, PII masking, consent policies, deterministic replay for audits.
  • Interop. Direct SQL, gRPC/HTTP, WASM for sandboxing rules, JVM/.NET/FFI bindings.
  • Tooling. Package manager, codegen for APIs/DBs, migration tool, linter, formatter, test & property-based testing, profiler.
  • Deployment. First-class batchcron, and service targets; containers; observability (OpenTelemetry).



Let me give you a tiny taste of the syntax


1) Module, types, and invariants


module finance.calculations


use time.{date, period}

use money.{money, rate%, rounding}

use data.{table, from_csv, to_csv}

use sql

use effects(db, fs)


type VatRate = rate%  // e.g., 19%, 7%


record VatBreakdown {

  net   : money<EUR>

  vat   : money<EUR>

  gross : money<EUR>

} ensure gross == net + vat


fn vat_from_net(net: money<EUR>, rate: VatRate): VatBreakdown {

  let vat   = net * rate with rounding(scale=2, mode=Bankers)

  let gross = net + vat

  return { net, vat, gross }

}


fn vat_from_gross(gross: money<EUR>, rate: VatRate): VatBreakdown {

  let vat   = gross * (rate / (1% + rate)) with rounding(scale=2)

  let net   = gross - vat

  return { net, vat, gross }

}


2) Interest (simple & compound) with precise decimals


fn simple_interest(p: money<EUR>, annual: rate%, years: decimal(10,4))

  -> { amount: money<EUR>, interest: money<EUR> } {

  let interest = p * annual * years with rounding(scale=2)

  return { amount: p + interest, interest }

}


fn compound_interest(

  p: money<EUR>, annual: rate%, years: decimal(10,4), n: u32  // compounds/year

) -> { amount: money<EUR>, interest: money<EUR> } {

  require n >= 1

  let r = annual / n%

  // Deterministic loop (avoids platform EXP/LOG diffs)

  var a = p

  repeat (years * n).floor() times { a = a * (1 + r) with rounding(scale=8) }

  let interest = (a - p) with rounding(scale=2)

  return { amount: (p + interest), interest }

}


3) Inline SQL over a declared schema (typed!)


table invoices(id u64, customer text, net money<EUR>, rate VatRate, issued date);


service reporting @effects(db) {

  // Typed query; DB driver generated from schema

  fn monthly_vat(yyyy_mm: text) -> table(customer text, vat money<EUR>) {

    return sql """

      select customer, sum(net * (rate))::money<EUR> as vat

      from invoices

      where to_char(issued, 'YYYY-MM') = :yyyy_mm

      group by customer

    """;

  }

}


4) Streams & sagas (for real systems)


stream payments(topic="payments", key=customer_id) 

  : record { customer_id u64, amount money<EUR>, ts timestamp };


actor settlement {

  state total_by_customer : map<u64, money<EUR>> = {};


  on payments as p {

    total_by_customer[p.customer_id] += p.amount with rounding(scale=2);

    if end_of_day() then persist_snapshot();

  }

}


saga close_month @effects(db) {

  step "lock-books" { sql "select lock_period(:month)"; }

  step "post-entries" compensates "post-entries-reverse" {

    // ... post GL entries

  }

  step "unlock" { sql "select unlock_period(:month)"; }

}


5) A tiny CLI that does the earlier calculator menu—concise, safe, testable


cli finance-calc {

  cmd vat-from-net (net: money<EUR>, rate: VatRate) {

    print vat_from_net(net, rate)

  }


  cmd vat-from-gross (gross: money<EUR>, rate: VatRate) {

    print vat_from_gross(gross, rate)

  }


  cmd simple-interest (p: money<EUR>, rate: rate%, years: decimal(10,4)) {

    print simple_interest(p, rate, years)

  }


  cmd compound-interest (p: money<EUR>, rate: rate%, years: decimal(10,4), n: u32=12) {

    print compound_interest(p, rate, years, n)

  }

}



Design choices (why)

  • Business-grade numeric integrity. Decimal everywhere, explicit rounding, deterministic math = no audit fights.
  • English-leaning names, modern structure. Readable to domain experts, compact enough for engineers.
  • Typed data + SQL + streams. Most business code is data plumbing, validation, and reporting—make it first-class.
  • Effect typing & sagas. Production systems live in the messy world of I/O and partial failure—make it explicit and safe.
  • Actors for services; batch is a citizen. COBOL did batch brilliantly; today we want batch + services + streams seamlessly.
  • Regulatory features. Built-in audit trails, PII policies, determinism switches, and replayability for compliance.



Conclusions 

The fictive Cobol-Next project demonstrates how a decades-old paradigm — descriptive business logic expressed in near-natural language — can be reimagined for the 21st century without sacrificing clarity or safety.

  1. Readable and Declarative by Design
    Cobol-Next retains COBOL’s original virtue: self-explanatory code. Yet it replaces the verbose “English-like” syntax with a concise, statically typed language resembling modern data-centric DSLs. Concepts such as record, money<Currency>, rate%, and decimal make financial semantics explicit while remaining human-readable.
  2. Strong Type Safety and Determinism
    Built-in monetary and percentage types eliminate rounding errors common in legacy COBOL or spreadsheet systems. Deterministic, banker’s-rounding arithmetic and currency consistency checking ensure correctness by construction — crucial for finance, accounting, and risk analytics.
  3. Interoperability and Portability
    The compiler may emit C99 or LLVM IR, producing native binaries that can integrate easily with existing ecosystems. This dual-backend design makes Cobol-Next suitable both for low-level embedded deployments and for cloud services compiled to WASM or LLVM targets.
  4. Structured Concurrency and Future-Proofing
    By treating each fn and record as first-class citizens and preparing for actor-style service constructs, Cobol-Next bridges the gap between business transaction logic and modern asynchronous systems. It is future-ready for integration with message buses, databases, and agentic AI workflows.
  5. Tooling and Transparency
    The compiler itself is small, transparent, and hackable — deliberately so. It encourages learning, experimentation, and extension: new backends, richer standard libraries, and even formal verification of financial flows.
  6. CSV and Reporting Integration
    The accompanying reporting engine showcases practical interoperability: Cobol-Next computations can be embedded in C pipelines for CSV/JSON analytics, effectively turning domain logic into executable financial reports.

Cobol-Next points toward a renaissance of domain-specific, semantically rich programming.

It suggests that future enterprise systems need not be monolithic COBOL rewrites nor ad-hoc script collections — but can be expressed in compact, type-safe, readable DSLs that compile down to efficient native code or LLVM IR.

In short, Cobol-Next re-awakens the spirit of COBOL for a new generation: human-centric in syntax, mathematically rigorous in semantics, and deeply compatible with today’s software architecture landscape.