<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Daniel Mori</title>
    <description>The latest articles on DEV Community by Daniel Mori (@daniel_mori).</description>
    <link>https://dev.to/daniel_mori</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4012702%2F61ccf6b6-c5e1-4bce-b738-dfae3201423f.jpg</url>
      <title>DEV Community: Daniel Mori</title>
      <link>https://dev.to/daniel_mori</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/daniel_mori"/>
    <language>en</language>
    <item>
      <title>The Shadow Ledger Pattern: How to Keep a Bank Live on FedNow During Core Maintenance Windows</title>
      <dc:creator>Daniel Mori</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:10:35 +0000</pubDate>
      <link>https://dev.to/daniel_mori/the-shadow-ledger-pattern-how-to-keep-a-bank-live-on-fednow-during-core-maintenance-windows-3o60</link>
      <guid>https://dev.to/daniel_mori/the-shadow-ledger-pattern-how-to-keep-a-bank-live-on-fednow-during-core-maintenance-windows-3o60</guid>
      <description>&lt;p&gt;Every core banking system has maintenance windows. Legacy systems — the kind running inside the majority of U.S. banks — have several per week. Planned downtime for log rotation, batch reconciliation, software updates. This is normal and expected.&lt;/p&gt;

&lt;p&gt;FedNow is not normal. FedNow requires 24/7/365 availability. No windows. No exceptions. If your bank is a FedNow participant, you cannot go offline to do maintenance.&lt;/p&gt;

&lt;p&gt;These two facts are in direct conflict. Resolving that conflict is the job of the Shadow Ledger.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem in Detail
&lt;/h2&gt;

&lt;p&gt;When a FedNow credit transfer arrives at your bank:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The FedNow gateway receives the ISO 20022 &lt;code&gt;pacs.008&lt;/code&gt; message&lt;/li&gt;
&lt;li&gt;The message must be accepted or rejected within 20 seconds&lt;/li&gt;
&lt;li&gt;If accepted, funds must be credited to the recipient's account&lt;/li&gt;
&lt;li&gt;The core banking system must record the transaction&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Steps 1–3 are time-critical. Step 4 requires the core banking system to be online.&lt;/p&gt;

&lt;p&gt;During a maintenance window, the core is unavailable. Under a naive integration, incoming FedNow payments would fail — the bank would be forced to return the payment, degrade its FedNow participation status, and potentially violate its service agreement with the Federal Reserve.&lt;/p&gt;

&lt;p&gt;The same problem existed in Brazil. When we integrated Santander Brazil into PIX in 2020, the bank's core had multiple weekly maintenance windows. PIX, like FedNow, required 24/7 availability. We couldn't eliminate the maintenance windows. We couldn't violate PIX's availability requirements. We had to architect around the conflict.&lt;/p&gt;

&lt;p&gt;The solution was the Shadow Ledger.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Shadow Ledger Is
&lt;/h2&gt;

&lt;p&gt;The Shadow Ledger is a real-time, in-memory (with durable write-ahead log) transaction store that sits between the payment network and the core banking system. It is not a replacement for the core ledger. It is a buffer — a temporary authoritative record of in-flight and recently settled transactions that allows the system to function when the core is unavailable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FedNow Gateway
      │
      ▼
Anti-Corruption Layer
      │
      ▼
Shadow Ledger  ◄──── always available
      │
      ▼
Core Banking System  ◄──── sometimes unavailable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the core is online, the Shadow Ledger acts as a pass-through: transactions are recorded in both the Shadow Ledger and the core in near-real-time.&lt;/p&gt;

&lt;p&gt;When the core is offline (maintenance window), the Shadow Ledger becomes the authoritative record. Incoming transactions are accepted, credited in the Shadow Ledger, and queued for reconciliation when the core comes back up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Data Model
&lt;/h2&gt;

&lt;p&gt;Each Shadow Ledger entry captures the minimum state needed to accept a payment and later reconcile it with the core:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;ShadowEntry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt;      &lt;span class="n"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// FedNow end-to-end ID&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt;      &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;// internal account identifier&lt;/span&gt;
    &lt;span class="nc"&gt;BigDecimal&lt;/span&gt;  &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;Currency&lt;/span&gt;    &lt;span class="n"&gt;currency&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;Instant&lt;/span&gt;     &lt;span class="n"&gt;receivedAt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;EntryState&lt;/span&gt;  &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;// PENDING | CREDITED | RECONCILED | FAILED&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt;      &lt;span class="n"&gt;rawIso20022&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// original pacs.008 payload&lt;/span&gt;
    &lt;span class="nc"&gt;Instant&lt;/span&gt;     &lt;span class="n"&gt;reconciledAt&lt;/span&gt;        &lt;span class="c1"&gt;// null until core confirms&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EntryState&lt;/code&gt; drives the reconciliation state machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PENDING&lt;/strong&gt; — received from FedNow, not yet processed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CREDITED&lt;/strong&gt; — credited in Shadow Ledger, core unavailable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RECONCILED&lt;/strong&gt; — confirmed by core, shadow entry can be archived&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FAILED&lt;/strong&gt; — core rejected on reconciliation (requires manual review)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Accepting Payments During a Maintenance Window
&lt;/h2&gt;

&lt;p&gt;The critical design decision: when the core is down, do you accept or reject incoming payments?&lt;/p&gt;

&lt;p&gt;Rejecting is safer from a consistency standpoint but violates FedNow availability requirements and creates a terrible customer experience. A payment that arrives at 2am during a maintenance window simply fails.&lt;/p&gt;

&lt;p&gt;Accepting requires you to make a commitment you can't immediately fulfill — you're promising the sender's bank that funds have been credited before your core says so. This requires confidence in your reconciliation path.&lt;/p&gt;

&lt;p&gt;We chose to accept. The reasoning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Shadow Ledger's write-ahead log gives us durable, crash-safe storage. The entry survives a power failure.&lt;/li&gt;
&lt;li&gt;Reconciliation is deterministic — the core will come back up, and we have the full ISO 20022 payload to replay.&lt;/li&gt;
&lt;li&gt;The only failure mode is a core rejection on reconciliation, which requires manual review regardless. That's a better outcome than a failed payment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The acceptance logic in the FedNow gateway handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Pacs008Message&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ShadowEntry&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ShadowEntry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromPacs008&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Always write to shadow ledger first&lt;/span&gt;
    &lt;span class="n"&gt;shadowLedger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;persist&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;PENDING&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coreAvailabilityProbe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAvailable&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;CoreResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coreAdapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;shadowLedger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="no"&gt;CREDITED&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;rejected&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Core is down — accept optimistically, queue for reconciliation&lt;/span&gt;
        &lt;span class="n"&gt;shadowLedger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="no"&gt;CREDITED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;reconciliationQueue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;enqueue&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PaymentResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ← the key decision&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Reconciliation
&lt;/h2&gt;

&lt;p&gt;When the core comes back up, the reconciliation job replays every CREDITED entry in arrival order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Scheduled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixedDelay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30_000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;reconcile&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;coreAvailabilityProbe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAvailable&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ShadowEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shadowLedger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CREDITED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ShadowEntry&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;CoreResult&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coreAdapter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;credit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;shadowLedger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="no"&gt;RECONCILED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;shadowLedger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateState&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="no"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;alertOps&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Reconciliation failed for {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// Leave as CREDITED — will retry next cycle&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Idempotency is critical here. The core adapter must handle duplicate credit attempts gracefully — the reconciliation job may replay an entry that was partially processed before a crash. In OpenFedNow, each adapter implements an idempotency key derived from the FedNow end-to-end transaction ID.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Write-Ahead Log
&lt;/h2&gt;

&lt;p&gt;The Shadow Ledger's durability guarantee comes from a write-ahead log (WAL). Before any state transition is confirmed to the caller, it is written to the WAL. On startup, the WAL is replayed to reconstruct the in-memory state.&lt;/p&gt;

&lt;p&gt;This gives us crash safety: if the JVM dies between accepting a payment and writing to the database, the WAL replay catches it on restart.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ShadowEntry&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;wal&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;append&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WalEntry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;       &lt;span class="c1"&gt;// durable first&lt;/span&gt;
    &lt;span class="n"&gt;inMemoryStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transactionId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// then in-memory&lt;/span&gt;
    &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// then database (async ok)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database write is intentionally async. Consistency flows from WAL → memory → database, not the other way around.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Buys You
&lt;/h2&gt;

&lt;p&gt;A bank running this pattern can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Schedule core maintenance windows normally&lt;/li&gt;
&lt;li&gt;Accept FedNow payments 24/7 without degradation&lt;/li&gt;
&lt;li&gt;Reconcile automatically when the core returns&lt;/li&gt;
&lt;li&gt;Handle core crashes (not just planned maintenance) with the same recovery path&lt;/li&gt;
&lt;li&gt;Meet FedNow's availability SLA without architectural surgery to the core&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At Santander Brazil, this pattern ran in production for over 18 months. The reconciliation queue was non-empty on every maintenance window. It processed without a single data loss event.&lt;/p&gt;




&lt;h2&gt;
  
  
  OpenFedNow
&lt;/h2&gt;

&lt;p&gt;This pattern is implemented in &lt;a href="https://github.com/danielsmori/open-fednow" rel="noopener noreferrer"&gt;OpenFedNow&lt;/a&gt;, a free open-source dual-rail middleware framework for FedNow and RTP integration. The Shadow Ledger is Layer 3 of five. Pre-built adapters for Fiserv, FIS, and Jack Henry. Apache 2.0.&lt;/p&gt;

&lt;p&gt;Technical whitepaper (with full architecture documentation): &lt;a href="https://doi.org/10.5281/zenodo.21114113" rel="noopener noreferrer"&gt;doi.org/10.5281/zenodo.21114113&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Questions, issues, or PRs welcome.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Daniel Stelzer Mori is a payments architect and the creator of OpenFedNow. He served as Application Architect for Santander Brazil's PIX integration from 2019–2021.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>fintech</category>
      <category>opensource</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
