Skip to content

danielsmori/open-fednow

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

89 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenFedNow — Legacy Core to U.S. Instant Payment Rails

Open-source middleware connecting legacy core banking systems to U.S. instant payment rails — FedNow and RTP — through a reusable, rail-agnostic core framework.

License Status Java Spring Boot

73% of U.S. financial institutions cite legacy core banking systems as a moderate-to-severe obstacle to FedNow participation because their core systems — Fiserv, FIS, Jack Henry — were built for batch processing, not 24/7 real-time settlement. This framework bridges the gap without touching the core.

Sandbox / reference implementation. The reusable core framework — Shadow Ledger, SyncAsyncBridge, Saga orchestration, idempotency, reconciliation, dual-rail Layer 1 (FedNow + RTP), and all three vendor adapters — is implemented and tested. Live rail connectivity remains credential-, certification-, and institution-dependent. See docs/known-limitations.md and the Production Boundaries section for the full gap analysis.


What works today

Component Status
Five-layer architecture ✅ Implemented
ISO 20022 message models (pacs.008, pacs.002, pacs.004, camt.056/029) ✅ Implemented
Cancellation handling — inbound camt.056 → camt.029 with state-keyed decision matrix ✅ Implemented on both rails; CNCL reverses Shadow Ledger credit and terminates saga. See ADR-0007
Fraud pre-screening — FraudScreeningPort with rule-based default (amount cap, debtor velocity, denylist) ✅ Disabled by default; opt-in via openfednow.fraud.enabled=true. BLOCK returns RJCT FRAD before any side effects. Institutions swap in their own port for production. See ADR-0008
SandboxAdapter — all scenarios: ACSC, RJCT, ACSP, timeout ✅ Implemented
MockVendorAdapter — in-memory balance ledger, configurable failure modes ✅ Implemented; CoreBankingAdapterContractTest enforces adapter contract
CoreBankingAdapter contract ✅ Implemented
Shadow Ledger — Redis-backed, WATCH/MULTI/EXEC optimistic locking ✅ Implemented + tested
Shadow Ledger endpoint wiring (inbound + outbound) ✅ Implemented in sandbox/reference mode
24/7 Bridge Mode — queues payments during core maintenance window ✅ Implemented + tested
Reconciliation — replay and sync after core returns online ✅ Implemented + tested
Reconciliation pagination — keyset-paginated account scan for large institutions ✅ Configurable batch size (default 500); memory stays flat regardless of pending-account count
Saga orchestration — compensation on core rejection ✅ Implemented + tested
Idempotency — Redis + PostgreSQL dual-write, 48h window ✅ Implemented + tested
Concurrent overdraft prevention under load ✅ Tested (race-condition suite)
Send-side (outbound) payment flow ✅ Implemented in sandbox/reference mode
Admin auth — HTTP Basic on /admin/* ✅ Implemented as reference configuration
Admin audit log — every /admin/** access recorded to PostgreSQL ✅ Implemented; both GRANTED and DENIED captured, surfaced via GET /admin/audit-log
Admin query endpoints — saga state, balances, reconciliation history GET /admin/sagas[/{txId}], /admin/accounts/{id}/balance, /admin/reconciliation-runs[/{id}], /admin/audit-log
Saga recovery on application restart ApplicationReadyEvent listener; dispatches each non-terminal saga by state (compensate, advance, finalize)
Saga timeout monitor — auto-compensate stalled sagas @Scheduled sweep; ISO 20022 XPIR reason; saga.timeout Micrometer counter
Balance seeding from core on startup ✅ Configurable account list seeded via SETNX; POST /admin/shadow-ledger/seed for on-demand re-seed
Idempotency cleanup — scheduled sweep of expired Postgres rows ✅ Configurable TTL (default 48h) and sweep cadence (default 60min)
Rate limiting — per-client on /fednow/** and /rtp/** POSTs ✅ Resilience4j RateLimiter per IP / X-Forwarded-For; 429 + Retry-After; gateway.rate_limited counter
Dual-rail architecture (FedNow + RTP) ✅ ISO 20022 foundation; Layer 1 varies, Layers 2–4 rail-agnostic; source rail persisted on saga_state
RTP Layer 1 — inbound XML, outbound XML, TCH cert validation hook, sandbox + HTTP client ✅ Implemented and tested in reference mode; symmetric with FedNow Layer 1
Optional Kafka event bus — PaymentEventPublisher, 6 event types ✅ Implemented (disabled by default; no Kafka required)
Event schema versioning — schemaVersion field + X-Schema-Version / X-Event-Type headers ✅ Implemented; JSON Schema in docs/event-schemas/; strategy documented in ADR-0006
Vendor adapters (Fiserv, FIS, Jack Henry) ✅ All three implemented (OAuth 2.0, ISO 20022 code mapping, WireMock tests); Fiserv + FIS via REST/JSON, Jack Henry via jXchange SOAP
Live FedNow connectivity (Fed PKI, mTLS, message signing) 🔲 Credential/certification-dependent; simulator-compatible HTTP client implemented
Live RTP connectivity (TCH network, TCH PKI certificates) 🔲 TCH onboarding/certification-dependent; HttpRtpClient and full XML pipeline implemented

See docs/known-limitations.md for the full gap analysis.


Architecture

flowchart TD
    FN([FedNow Service\npacs.008 — 20s window])
    RTP([RTP Network — TCH\npacs.008 XML — full Layer 1])

    FN  --> GW["Layer 1 — API Gateway\nFedNowGateway · RtpGateway\nISO 20022 parsing · idempotency · correlation IDs"]
    RTP --> GW

    GW --> CHK{Core online?\nAvailabilityBridge\npoll / 30s}

    CHK -->|Yes| ACL["Layer 2 — Anti-Corruption Layer\nSyncAsyncBridge 15s timeout\nVendor protocol translation"]
    CHK -->|No — maintenance window| SL["Shadow Ledger\nRedis WATCH/MULTI/EXEC\nInteger cents · MAX_RETRY=3"]

    SL --> MQ["RabbitMQ\nmaintenance-window-transactions\nDurable · FIFO · DLQ"]
    MQ --> ACSP([pacs.002 ACSP\nreturned to rail])

    ACL --> PE["Layer 3 — Processing Engine\nSaga state machine · Idempotency\nCircuit breakers"]

    PE -->|ACSC| CORE["Layer 5 — Core Banking\nFiserv · FIS · Jack Henry\nunchanged"]
    PE -->|RJCT| COMP["Saga compensation\nShadowLedger.reverseDebit\npacs.004 return"]

    CORE -->|Core returns online| RECON["ReconciliationService.reconcile\nReplay in timestamp order\nZero-discrepancy tolerance"]
    RECON --> CORE
Loading

Key documents

Document What it answers
docs/known-limitations.md Current implementation boundaries, credential-dependent live connectivity, and production-readiness gaps
docs/rtp-compatibility.md RTP Layer 1 status: implemented and tested in reference mode, symmetric with FedNow at the framework level; TCH live connectivity pending institutional credentials
docs/adr/0005-dual-rail-architecture-fednow-rtp.md Decision: keep Layers 2–4 rail-agnostic
docs/adr/0004-eventual-consistency-shadow-ledger-and-core.md Why eventual consistency, why not 2PC
docs/shadow-ledger.md How the Shadow Ledger works, failure modes
docs/adr/0001-optimistic-locking-shadow-ledger-debits.md Why WATCH/MULTI/EXEC, the Lettuce caveat
docs/adr/0003-provisional-acceptance-acsp.md Why ACSP is returned, the exposure window
docs/saga-pattern.md Compensation path when core rejects post-ACSP

The Problem

The Federal Reserve's FedNow Instant Payment Service launched in July 2023. As of early 2026, only approximately 1,500 of the nation's 10,000+ financial institutions have connected — roughly 16% of the eligible ecosystem.

The barrier is not cost or intent. Industry research documents that 73% of U.S. financial institutions cite legacy core banking systems as a moderate-to-severe obstacle to FedNow participation (U.S. Faster Payments Council / Finzly, 2024). The core banking platforms that power the majority of U.S. banks — Fiserv, FIS, and Jack Henry — were designed for batch-processing cycles, not 24/7 real-time settlement.

This creates four fundamental incompatibilities:

  • Processing model mismatch — Legacy systems process in batches; FedNow requires sub-20-second event-driven responses
  • Availability mismatch — Legacy systems have maintenance windows; FedNow operates 24/7/365
  • Protocol mismatch — Legacy systems use proprietary APIs; FedNow uses ISO 20022 REST/JSON messaging
  • Concurrency mismatch — Legacy systems were not designed for high-volume simultaneous transaction loads

These incompatibilities cannot be resolved by adding API endpoints. They require purpose-built architectural layers that bridge the two paradigms — allowing institutions to participate in FedNow without replacing their existing core systems.


The Solution

OpenFedNow is a five-layer middleware framework that resolves each of these incompatibilities through architectural patterns drawn from production-scale instant payment integration experience.

The framework is designed around a key insight: based on a count of production Java source lines in src/main/java (excluding tests, documentation, and generated files), approximately 87% of the integration architecture is shared across all financial institutions. Only the Core Banking Adapter — approximately 13% of the total engineering scope — varies per core banking vendor. This means that building adapters for the three dominant U.S. platforms covers the majority of the market without duplicating the underlying work.

Core Banking Platform Coverage

Vendor U.S. Bank Market Share U.S. Credit Union Share
Fiserv (DNA, Precision, Premier, Cleartouch) 42% 31%
Jack Henry (SilverLake, Symitar, CIF 20/20) 21% 12%
FIS (Horizon, IBS) 9%
Big Three combined >70%

Source: Federal Reserve Bank of Kansas City, Market Structure of Core Banking Services Providers, March 2024.

Three adapter implementations make the complete framework available to thousands of institutions.


Quick Start

Prerequisites: Java 17+, Docker.

git clone https://github.com/danielsmori/open-fednow
cd open-fednow
docker-compose up -d redis rabbitmq    # Redis + RabbitMQ (Kafka is optional)
mvn spring-boot:run                    # → http://localhost:8080

Once the app is running (look for Started OpenFedNowApplication in the console), verify it's up:

curl http://localhost:8080/fednow/health
# → OpenFedNow Gateway — operational

Then run the demo:

./demo/run-demo.sh

That's it. The script runs all four scenarios — ACSC, RJCT, ACSP, reconcile — and prints pass/fail for each.

What the demo does (step by step)

1. Send a payment (core online)

curl -s -X POST http://localhost:8080/fednow/receive \
  -H "Content-Type: application/json" \
  -d '{
    "messageId":                  "MSG-DEMO-001",
    "endToEndId":                 "E2E-DEMO-001",
    "transactionId":              "TXN-DEMO-001",
    "interbankSettlementAmount":  250.00,
    "interbankSettlementCurrency":"USD",
    "creditorAccountNumber":      "ACC-DEMO-12345"
  }'
{"transactionStatus":"ACSC","originalEndToEndId":"E2E-DEMO-001","originalTransactionId":"TXN-DEMO-001"}

ACSC — AcceptedSettlementCompleted. The sandbox core accepted the transfer.

2. Test a rejection scenario

The sandbox adapter routes by creditorAccountNumber prefix. No config change needed:

curl -s -X POST http://localhost:8080/fednow/receive \
  -H "Content-Type: application/json" \
  -d '{
    "messageId":                  "MSG-DEMO-002",
    "endToEndId":                 "E2E-DEMO-002",
    "transactionId":              "TXN-DEMO-002",
    "interbankSettlementAmount":  250.00,
    "interbankSettlementCurrency":"USD",
    "creditorAccountNumber":      "RJCT_FUNDS_ACC-DEMO-67890"
  }'
{"transactionStatus":"RJCT","originalEndToEndId":"E2E-DEMO-002","rejectReasonCode":"AM04"}

RJCT / AM04 — Rejected, insufficient funds. Other prefixes: RJCT_ACCT_ (AC01), RJCT_CLOSED_ (AC04), TOUT_ (triggers the ACSP provisional-acceptance path).

3. Trigger provisional acceptance (ACSP)

The TOUT_ prefix makes the sandbox adapter return a TIMEOUT status immediately. The SyncAsyncBridge maps this to a provisional acceptance without waiting. No app restart needed:

curl -s -X POST http://localhost:8080/fednow/receive \
  -H "Content-Type: application/json" \
  -d '{
    "messageId":                  "MSG-DEMO-003",
    "endToEndId":                 "E2E-DEMO-003",
    "transactionId":              "TXN-DEMO-003",
    "interbankSettlementAmount":  300.00,
    "interbankSettlementCurrency":"USD",
    "creditorAccountNumber":      "TOUT_ACC-DEMO-12345"
  }'
{"transactionStatus":"ACSP","originalEndToEndId":"E2E-DEMO-003","originalTransactionId":"TXN-DEMO-003"}

ACSP — AcceptedSettlementInProcess. FedNow received a response within its 20-second window. The PEND_ prefix works identically. For the full maintenance-window path (core offline → ACSP → RabbitMQ queue → reconcile), restart the app with OPENFEDNOW_SANDBOX_CORE_AVAILABLE=false mvn spring-boot:run.

4. Trigger reconciliation

No restart needed — reconcile against the same running instance:

curl -s -u admin:changeme -X POST http://localhost:8080/admin/reconcile
{
  "transactionsReplayed": 0,
  "discrepanciesDetected": 0,
  "reconciliationSuccessful": true,
  "summary": "Clean reconciliation: 0 entries confirmed across all accounts"
}

transactionsReplayed: 0 — H2 in-memory resets on restart, so this run starts with a clean ledger. In a PostgreSQL deployment, bridge-mode transactions accumulated during the offline window appear here as transactionsReplayed: N. The reconciliation path is fully exercised in BridgeModeIntegrationTest and ReconciliationServiceIntegrationTest using Testcontainers.


What a maintenance window looks like in production

Annotated log output from the full cycle: core goes offline, payment arrives, core returns, reconciliation runs.

# 01:58 — AvailabilityBridge detects core going offline
WARN  [scheduling-1] AvailabilityBridge  Core banking system OFFLINE — entering bridge mode,
                                          transactions will be queued for replay

# 02:03 — $300 payment arrives during maintenance window
INFO  [http-nio-8080-exec-2] MessageRouter  Inbound credit transfer received amount=300.00 currency=USD
INFO  [http-nio-8080-exec-2] AvailabilityBridge  Bridge mode active — queuing inbound payment e2e=E2E-MAINT-001
INFO  [http-nio-8080-exec-2] AvailabilityBridge  Transaction queued for core replay transactionId=E2E-MAINT-001
INFO  [http-nio-8080-exec-2] MessageRouter  Inbound credit transfer status=ACSP rejectCode=null
# → pacs.002 ACSP returned to FedNow in 6ms. FedNow satisfied.

# 02:03 — RabbitMQ queue depth: 1
#   maintenance-window-transactions: messages=1, consumers=0

# 05:47 — Core returns online
INFO  [scheduling-1] AvailabilityBridge  Core banking system ONLINE — exiting bridge mode,
                                          reconciliation pending

# 05:47 — Operator triggers reconciliation (or automatic on core-return event)
INFO  [http-nio-8080-exec-5] ReconciliationService  Reconciliation cycle starting
INFO  [http-nio-8080-exec-5] ReconciliationService  Reconciliation found 1 accounts with pending entries
INFO  [http-nio-8080-exec-5] ReconciliationService  Reconciliation cycle complete
                                                      replayed=1 discrepancies=0 success=true

# Response to POST /admin/reconcile
{
  "transactionsReplayed": 1,
  "discrepanciesDetected": 0,
  "reconciliationSuccessful": true,
  "summary": "Clean reconciliation: 1 entries confirmed across all accounts"
}
# shadow_ledger_transaction_log: core_confirmed = TRUE
# Balance in Redis matches core confirmed balance. Divergence window closed.

Test coverage

The test suite uses Testcontainers (PostgreSQL + Redis + RabbitMQ) for all integration tests — no mocking of infrastructure.

Test class What it covers
ShadowLedgerConcurrencyTest 10 concurrent debits, overdraft prevention under race conditions, credit atomicity, audit row count matches successful ops
ReplayOrderingIntegrationTest FIFO ordering across 5 queued messages, targeted replay without confirming unlisted entries, poison message → DLQ without blocking queue, idempotent re-replay
BridgeModeIntegrationTest Full bridge mode cycle: payment queued during offline window, RabbitMQ message verified, reconciliation run, core_confirmed flipped
SagaCompensationIntegrationTest Saga initiated → INITIATED state persisted, compensation reverses Shadow Ledger debit, double-compensation is no-op, reason code stored
IdempotencyServiceIntegrationTest Redis fast-path dedup, DB fallback when Redis key absent, concurrent recordOutcome calls produce exactly one row, 48h TTL
ReconciliationServiceIntegrationTest Discrepancy detection → alert + Shadow Ledger overwrite, clean run, multi-account run
RabbitMqDlqTest DLQ topology declared on startup, nack-without-requeue routes to DLQ, main queue unaffected
FlywayMigrationTest All migrations apply cleanly to a fresh PostgreSQL container

The Shadow Ledger in Action

The Shadow Ledger is what makes 24/7 FedNow participation possible with a legacy core that has nightly maintenance windows. It's the most defensible piece of this architecture, so here's what it actually does.

The problem it solves

Legacy core: offline 2am–6am for batch processing
FedNow:      $300 payment arrives at 3am
Without Shadow Ledger: institution doesn't respond → FedNow marks you unavailable
With Shadow Ledger:    institution responds ACSP in under 1 second → FedNow happy

Concrete behavior, step by step

Account balance is seeded from the core at startup (stored in Redis as integer cents):

$ redis-cli SET balance:ACC-12345 5000000     # $50,000.00
$ redis-cli GET balance:ACC-12345
"5000000"

Balances are stored as integer cents — no floating-point arithmetic on money.


Stage 1: 11pm — Core is online. A $250 payment arrives.

// Inside MessageRouter.routeInbound() when core is available:
shadowLedger.applyDebit("ACC-12345", new BigDecimal("250.00"), "TXN-0001");

The debit uses Redis WATCH / MULTI / EXEC — atomic, safe under concurrent load:

WATCH  balance:ACC-12345
GET    balance:ACC-12345           → "5000000"
MULTI
  SET  balance:ACC-12345  4975000  ← $50,000 – $250 = $49,750
EXEC                               → success (key unchanged since WATCH)
$ redis-cli GET balance:ACC-12345
"4975000"                          # $49,750.00

An audit row is written to PostgreSQL:

shadow_ledger_transaction_log:
  transaction_id  = TXN-0001
  type            = DEBIT
  amount          = 250.00
  balance_before  = 50000.00
  balance_after   = 49750.00
  core_confirmed  = FALSE          ← pending core confirmation

pacs.002 ACSC is returned to FedNow. The core is contacted and confirms. core_confirmed flips to TRUE. The Shadow Ledger and core are in sync.


Stage 2: 2am — Core goes offline for scheduled maintenance.

The AvailabilityBridge polls every 30 seconds and detects the transition:

[WARN] Core banking system OFFLINE — entering bridge mode,
       transactions will be queued for replay

A $300 payment arrives at 2:47am.

// AvailabilityBridge.isInBridgeMode() == true
// Balance check passes against Shadow Ledger: $49,750 > $300
shadowLedger.applyDebit("ACC-12345", new BigDecimal("300.00"), "TXN-0002");
availabilityBridge.queueForCoreProcessing("E2E-MAINT-001", serializedPacs008);
$ redis-cli GET balance:ACC-12345
"4945000"                          # $49,450.00 — debit applied to Shadow Ledger

$ curl -s -u guest:guest \
    'http://localhost:15672/api/queues/%2F/maintenance-window-transactions' \
    | python3 -c "import sys,json; q=json.load(sys.stdin); print('queued:', q['messages'])"
queued: 1

pacs.002 ACSP is returned to FedNow immediately — well within the 20-second window. The core never saw this request. FedNow has no idea the core was offline.


Stage 3: 6am — Core comes back online.

[INFO] Core banking system ONLINE — exiting bridge mode, reconciliation pending
$ curl -s -u admin:changeme -X POST http://localhost:8080/admin/reconcile
{
  "transactionsReplayed": 1,
  "discrepanciesDetected": 0,
  "reconciliationSuccessful": true,
  "summary": "Clean reconciliation: 1 entries confirmed across all accounts"
}

The ReconciliationService:

  1. Finds all accounts with core_confirmed = FALSE entries
  2. Fetches the authoritative balance from the core for each account
  3. If the Shadow Ledger balance matches: marks entries confirmed, done
  4. If there's a discrepancy (e.g., the core processed something OpenFedNow didn't know about): overwrites the Shadow Ledger with the core's figure, logs a RECONCILIATION row, and alerts — zero discrepancy tolerance
$ redis-cli GET balance:ACC-12345
"4945000"                          # $49,450.00 — confirmed by core

The institution was available to FedNow for the entire 4-hour maintenance window. Every payment was accepted and the ledger is correct.

What protects against overdrafts under concurrent load

Three payments arrive simultaneously at 3am for the same account:

Thread 1: WATCH balance:ACC-12345 → GET "4945000" → MULTI → SET "4895000" → EXEC ✓
Thread 2: WATCH balance:ACC-12345 → GET "4945000" → EXEC returns [] (conflict) → retry
Thread 3: WATCH balance:ACC-12345 → GET "4895000" → MULTI → SET "4845000" → EXEC ✓
Thread 2: WATCH balance:ACC-12345 → GET "4845000" → MULTI → SET "4795000" → EXEC ✓

Each thread retries until its EXEC succeeds. The balance is always consistent. See ADR-0001 for the full analysis including the Lettuce empty-list caveat.


Architecture

The framework is structured as five independent layers. Each layer addresses a specific dimension of the legacy-to-real-time incompatibility.

┌───────────────────────────┐   ┌───────────────────────────┐
│  FedNow Service           │   │  RTP Network — TCH        │
│  Federal Reserve          │   │  ISO 20022 XML; live TCH  │
│  ISO 20022 JSON / HTTPS   │   │  credentials pending      │
└─────────────┬─────────────┘   └─────────────┬─────────────┘
              │                               │
┌─────────────▼───────────────────────────────▼─────────────┐
│         LAYER 1 — API Gateway & Security  ★ rail varies   │
│  FedNowGateway · RtpGateway (symmetric; full Layer 1)       │
│  TLS mutual auth · PKI certificates · Rate limiting        │
│  Fraud pre-screening · pacs.008 / pacs.002 routing         │
└────────────────────────┬───────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────────┐
│    LAYER 2 — Anti-Corruption Layer / Core Banking Adapter ★  │
│  ISO 20022 ↔ Vendor protocol translation                     │
│  Sync-to-async bridge · Vendor-specific adapters             │
│  [ Fiserv ] [ FIS ] [ Jack Henry ] [ IBM z/OS ]              │
└────────────────────────┬────────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────────┐
│       LAYER 3 — Real-Time Processing Engine                  │
│  Saga orchestration · Idempotency framework                  │
│  Distributed cache · Circuit breakers                        │
└────────────────────────┬────────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────────┐
│    LAYER 4 — Shadow Ledger & 24/7 Availability Bridge ★      │
│  Real-time balance tracking · Async message queuing          │
│  Maintenance window handling · Reconciliation service        │
└────────────────────────┬────────────────────────────────────┘
                         │
┌────────────────────────▼────────────────────────────────────┐
│         LAYER 5 — Legacy Core Banking (unchanged)            │
│  Fiserv · FIS · Jack Henry · IBM z/OS — no modification      │
└─────────────────────────────────────────────────────────────┘

★ Rail varies at Layer 1 only — Layers 2–4 are rail-agnostic. Novel contributions: the Anti-Corruption Layer and Shadow Ledger resolve the two hardest problems in legacy payment integration.

Layer Descriptions

Layer 1 — API Gateway & Security The only layer that varies between payment rails. FedNowGateway handles FedNow-specific connectivity: Federal Reserve PKI certificates, JSON envelope parsing, and REST/HTTPS transport. RtpGateway is implemented in reference mode and is symmetric with FedNowGateway: it accepts RTP ISO 20022 XML via RtpXmlParser, invokes the TCH certificate-validation hook via CertificateManager, and routes parsed messages through the same rail-agnostic pipeline. Outbound transfers are handled by RtpClient (XML serialization via RtpXmlSerializer, HTTP via HttpRtpClient when RTP_ENDPOINT is set). Live TCH connectivity requires institution-provided PKI credentials and private-network transport — the same class of dependency as Fed PKI for FedNow. Both gateways deliver the same Pacs008Message to MessageRouter — Layers 2–4 have no knowledge of which rail the message arrived on. Also handles rate limiting and fraud pre-screening on all inbound and outbound paths.

Layer 2 — Anti-Corruption Layer / Core Banking Adapter The architectural core of the framework. Translates between the modern ISO 20022 world (REST APIs, JSON, UTF-8) and the proprietary world of each core banking vendor (vendor-specific APIs, proprietary formats). Also manages the synchronous-to-asynchronous bridge: FedNow requires synchronous sub-20-second responses, but legacy core processing is inherently asynchronous. This layer decouples the two models. The vendor-specific adapter is the only component that varies between institutions (~13% of total scope).

Layer 3 — Real-Time Processing Engine Manages transaction orchestration and state across distributed systems. Key components: Saga pattern implementation for distributed transaction management (with compensation logic for rollback across multiple systems), idempotency key management to prevent duplicate processing, distributed cache for real-time balance availability, and circuit breakers to prevent cascade failures.

Layer 4 — Shadow Ledger & 24/7 Availability Bridge Resolves the hardest operational problem: legacy core systems go offline for maintenance while instant payment rails never do. The Shadow Ledger maintains a real-time view of available balances independently of the core system. Transactions arriving during core downtime are queued via async messaging and processed against the Shadow Ledger. A reconciliation service ensures the core ledger remains authoritative when it returns online. From the payment network's perspective, the institution is always available.

Layer 5 — Legacy Core Banking The existing core banking infrastructure — unchanged. A deliberate architectural principle: the framework works around legacy systems, never requiring their transformation. This protects the stability of core banking operations while enabling full FedNow participation.


ISO 20022 Compatibility

FedNow uses the ISO 20022 international messaging standard — the same standard used by Brazil's PIX instant payment system. This framework implements the following message types:

Message Type Description Direction
pacs.008.001.08 FI-to-FI Customer Credit Transfer Outbound (send)
pacs.002.001.10 Payment Status Report Inbound (confirmation/rejection)

Architectural Background

The five-layer architecture reflects a production-informed methodology based on experience with Santander Brazil's PIX integration (2020–2021). PIX launched on November 16, 2020 under Central Bank of Brazil mandate, with the national network reaching 175 million registered users and a single-day record of 313.3 million transactions across all participating institutions.

The core architectural problem — connecting legacy batch-processing systems to a 24/7 real-time payment network under ISO 20022 — represents the same class of legacy-to-real-time integration challenge across PIX, FedNow, and RTP, though each rail has its own network, certification, message-envelope, and regulatory requirements. The methodology underlying the Anti-Corruption Layer, Shadow Ledger, and Saga orchestration approach was applied in a production-scale instant-payment environment and is adapted here to U.S.-specific rails, vendors, credentials, and certification requirements.

The Santander PIX platform is proprietary to Santander Brazil. OpenFedNow is a new, independent implementation built from the ground up for the U.S. FedNow/RTP context, with Fiserv, FIS, and Jack Henry adapters replacing the original mainframe adapters.


Project Structure

openfednow/
├── src/main/java/io/openfednow/
│   ├── gateway/              # Layer 1 — API Gateway & Security
│   │   ├── FedNowGateway.java
│   │   ├── FedNowGateway.java · RtpGateway.java             # Dual-rail Layer 1 (symmetric)
│   │   ├── RtpXmlParser.java · RtpXmlSerializer.java       # pacs.008/pacs.002 XML, XXE-protected
│   │   ├── RtpClient.java · HttpRtpClient.java · SandboxRtpClient.java · RtpClientConfig.java
│   │   ├── FedNowClient.java · HttpFedNowClient.java · SandboxFedNowClient.java · FedNowClientConfig.java
│   │   ├── CertificateManager.java   # Fed PKI + TCH PKI validation (no-op in sandbox)
│   │   ├── MessageRouter.java                  # Routes both rails; tracks source Rail on every saga
│   │   ├── FedNowGatewayValidation.java        # ISO 20022 field validation + structured error handler
│   │   ├── AdminController.java                # /admin endpoints (HTTP Basic + ADMIN role)
│   │   │                                       #   POST /admin/reconcile · /admin/reconciliation-runs
│   │   │                                       #   GET  /admin/sagas[/{txId}] · /admin/audit-log
│   │   │                                       #   GET  /admin/accounts/{id}/balance · /admin/reconciliation-runs[/{id}]
│   │   │                                       #   POST /admin/shadow-ledger/seed
│   │   └── ratelimit/RateLimitFilter.java      # 429 + Retry-After on /fednow & /rtp; gateway.rate_limited metric
│   ├── acl/                  # Layer 2 — Anti-Corruption Layer
│   │   ├── core/
│   │   │   ├── CoreBankingAdapter.java      # Interface (4 methods)
│   │   │   ├── MessageTranslator.java
│   │   │   └── SyncAsyncBridge.java
│   │   └── adapters/
│   │       ├── SandboxAdapter.java          # Functional — scenario routing by prefix
│   │       ├── MockVendorAdapter.java        # Functional — in-memory ledger, configurable failures
│   │       ├── FiservAdapter.java           # Implemented — OAuth 2.0, ISO 20022 code mapping, WireMock tests
│   │       ├── FisAdapter.java              # Implemented — OAuth 2.0, ISO 20022 code mapping, WireMock tests
│   │       └── JackHenryAdapter.java        # Implemented — jXchange SOAP, OAuth 2.0, ISO 20022 code mapping, WireMock tests
│   ├── processing/           # Layer 3 — Real-Time Processing Engine
│   │   ├── saga/
│   │   │   ├── PaymentSaga.java
│   │   │   └── SagaOrchestrator.java
│   │   └── idempotency/
│   │       └── IdempotencyService.java
│   ├── shadowledger/         # Layer 4 — Shadow Ledger & Bridge
│   │   ├── ShadowLedger.java
│   │   ├── AvailabilityBridge.java
│   │   └── ReconciliationService.java
│   ├── events/               # Optional Kafka event bus
│   │   ├── PaymentEvent.java             # Record; 6-value EventType enum
│   │   ├── PaymentEventPublisher.java    # Interface — fire-and-forget
│   │   ├── NoOpPaymentEventPublisher.java    # Default (kafka.enabled=false)
│   │   ├── KafkaPaymentEventPublisher.java   # Active when kafka.enabled=true
│   │   └── KafkaConfig.java                  # Topic declaration
│   └── iso20022/             # ISO 20022 message models
│       ├── Pacs008Message.java
│       └── Pacs002Message.java
├── docs/
│   ├── architecture.md
│   ├── shadow-ledger.md
│   ├── anti-corruption-layer.md
│   ├── saga-pattern.md
│   ├── iso20022-mapping.md
│   ├── known-limitations.md   # What is and isn't production-ready
│   ├── rtp-compatibility.md   # Dual-rail design: what's shared, what varies
│   └── adr/                  # Architecture Decision Records
│       ├── 0001-optimistic-locking-shadow-ledger-debits.md
│       ├── 0002-redis-shadow-ledger-over-direct-core-reads.md
│       ├── 0003-provisional-acceptance-acsp.md
│       ├── 0004-eventual-consistency-shadow-ledger-and-core.md
│       └── 0005-dual-rail-architecture-fednow-rtp.md
├── LICENSE                   # Apache 2.0
└── README.md

Production Boundaries

OpenFedNow is a working sandbox/reference implementation of the reusable core framework. It is not a production-ready banking product. The following items remain outside the current public implementation and require institutional access:

  • Production vendor adaptersFiservAdapter, FisAdapter, and JackHenryAdapter are all implemented with OAuth 2.0 authentication, vendor error code → ISO 20022 mapping, and WireMock integration tests. Each requires institution-specific credentials (OAuth client ID/secret, base URL) from the respective vendor. SandboxAdapter and MockVendorAdapter are functional; CoreBankingAdapterContractTest enforces the behavioral contract all adapters must satisfy.
  • Live FedNow connectivity — requires Federal Reserve PKI client certificates, mutual TLS, JWS message signing, and FedNow certification. HttpFedNowClient provides simulator-compatible HTTP transport; SandboxFedNowClient is the default for local development.
  • Live RTP connectivity — requires TCH institutional participation, TCH PKI certificates, and private-network transport. HttpRtpClient, RtpXmlSerializer, and the TCH certificate-validation hook are implemented in reference mode; the endpoint URL and certificates are institution-provided.
  • Institution-specific configuration — account mapping, IAM integration, reconciliation policies, compliance controls, and operational validation are institution-dependent and not included in this framework.

Known Limitations

See docs/known-limitations.md for the full analysis. Key architectural boundaries:

  • Single-instance Shadow Ledger. The WATCH/MULTI/EXEC optimistic locking is safe for one pod. Multi-pod deployments require a distributed lock per account or consistent-hash routing.
  • Admin credentials are reference defaults. /admin/* requires HTTP Basic (admin / changeme). Override via ADMIN_USERNAME and ADMIN_PASSWORD before any non-local deployment.
  • Post-reconciliation reversals are customer-visible. If the core rejects a provisionally accepted transaction, a pacs.004 return goes back to FedNow. The sender's institution sees a credit followed by a return.

Roadmap

Phase 1 — Core Framework Foundation Established

  • Five-layer architecture: Shadow Ledger, SyncAsyncBridge, Saga orchestration, idempotency, reconciliation
  • ISO 20022 message models; RTP inbound XML parser (pacs.008 with XXE protection)
  • MockVendorAdapter + CoreBankingAdapterContractTest; dual content-type RTP gateway
  • Full test suite; CI pipeline; Docker / docker-compose deployment

Phase 2 — Fiserv + FIS Adapters ✅ Complete

  • Fiserv DNA adapter (42% of U.S. banks, 31% of credit unions) — implemented
  • FIS Horizon / IBS adapter (9% of U.S. banks) — implemented
  • Big Three combined: >70% of U.S. banks covered

Phase 3 — Jack Henry Adapter ✅ Complete

  • Jack Henry SilverLake / Symitar adapter (jXchange SOAP) — implemented
  • Big Three reference adapters complete: Fiserv + FIS + Jack Henry collectively serve over 70% of U.S. banks according to KC Fed data; credit union coverage varies by vendor and remains institution-specific

Phase 3b — RTP Layer 1 ✅ Complete

  • Layer 1 implemented in reference mode, symmetric with FedNow: RtpXmlSerializer, HttpRtpClient, SandboxRtpClient, RtpClientConfig, TCH certificate-validation hook
  • RtpGateway inbound XML and outbound send paths fully wired; 305 tests passing
  • Submission to U.S. Faster Payments Council as reference integration pattern

Phase 4 — IBM z/OS Path & Knowledge Transfer (Month 31+)

  • IBM z/OS mainframe adapter for large institutions
  • Technical assistance program for community bank engineering teams

Contributing

Contributions are welcome. See CONTRIBUTING.md for guidelines.

Areas where contributions are especially valuable:

  • Core banking vendor adapter implementations
  • ISO 20022 message validation
  • Test coverage for Saga compensation logic
  • Documentation and integration guides

License

This project is licensed under the Apache License 2.0. See LICENSE for the full text.

The goal of the Apache 2.0 license is to ensure this framework is freely available to any U.S. financial institution — regardless of size — without licensing fees or restrictions.


References

About

Open-source middleware connecting legacy core banking systems to U.S. instant payment rails — FedNow and RTP.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors