<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Mukhammedali Berektassuly]]></title><description><![CDATA[Deep dives into System Design, Rust, and Web3 Architecture from a software architect's perspective.]]></description><link>https://berektassuly.com</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1763468707373/9625a2ae-3e84-46ab-a154-a1a25ce06791.png</url><title>Mukhammedali Berektassuly</title><link>https://berektassuly.com</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 18 Apr 2026 19:35:56 GMT</lastBuildDate><atom:link href="https://berektassuly.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[chatpack: How I Compressed 11 Million Tokens Down to 850K for Chat Analysis]]></title><description><![CDATA[From manual scripts to automation: A Rust CLI tool for preparing chat exports for LLM analysis

The Problem Nobody Notices
Gemini refused to analyze my chat. "Failed to generate content. Please try again." And I needed to understand what patterns exi...]]></description><link>https://berektassuly.com/chatpack-compress-chat-exports-for-llm-rust</link><guid isPermaLink="true">https://berektassuly.com/chatpack-compress-chat-exports-for-llm-rust</guid><category><![CDATA[Rust]]></category><category><![CDATA[cli]]></category><category><![CDATA[llm]]></category><category><![CDATA[telegram]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[data processing]]></category><category><![CDATA[Open Source]]></category><dc:creator><![CDATA[Mukhammedali Berektassuly]]></dc:creator><pubDate>Wed, 24 Dec 2025 18:24:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766601675428/24cc3a71-736d-42b9-88ed-c9cec3005842.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>From manual scripts to automation: A Rust CLI tool for preparing chat exports for LLM analysis</em></p>
<hr />
<h2 id="heading-the-problem-nobody-notices">The Problem Nobody Notices</h2>
<p>Gemini refused to analyze my chat. <em>"Failed to generate content. Please try again."</em> And I needed to understand what patterns exist in how our community communicates.</p>
<p>I was trying to analyze a private developer group at a programming school where I study. I needed to identify trends in how students communicate and what topics come up most often. I needed to identify trends: why do so few students make it to the end? What changed when the platform was updated? What advice did graduates give?</p>
<p>I exported the chat from Telegram. 34 thousand messages, three years of history. Opened <code>result.json</code> - 26 megabytes. The problem isn't the file size. The problem is that 80% of those 26 megabytes is noise: JSON brackets, <code>"type": "message"</code> keys on every line, metadata about stickers you don't need.</p>
<p>Before this, I solved similar tasks with scripts that searched for keywords to find relevant sections. I sorted by nicknames and dates. The most annoying part - everything had to be done manually, and some data got lost due to this crude initial filtering.</p>
<p>That's how <a target="_blank" href="https://github.com/berektassuly/chatpack">chatpack</a> was born.</p>
<hr />
<h2 id="heading-what-chatpack-does">What chatpack Does</h2>
<pre><code class="lang-plaintext">Telegram JSON (11M tokens)
        ↓
    [chatpack]
        ↓
   CSV (850K tokens) → LLM → Insights
</code></pre>
<p>One command:</p>
<pre><code class="lang-bash">chatpack tg result.json -o context.csv
</code></pre>
<p>Result:</p>
<ul>
<li><p><strong>Input:</strong> 11.2M tokens (raw Telegram JSON)</p>
</li>
<li><p><strong>Output:</strong> 850K tokens (optimized CSV)</p>
</li>
<li><p><strong>Compression:</strong> 13x</p>
</li>
</ul>
<p>Now you can upload this CSV to LLM with an actual prompt:</p>
<pre><code class="lang-plaintext">Here's our team's chat history for 2023.

1. Identify the top 5 most discussed topics
2. Who was most active in each quarter?
3. Is there a correlation between time of day and message tone?
</code></pre>
<h3 id="heading-supported-platforms">Supported Platforms</h3>
<pre><code class="lang-bash">chatpack tg chat.json      <span class="hljs-comment"># Telegram</span>
chatpack wa chat.txt       <span class="hljs-comment"># WhatsApp</span>
chatpack ig message_1.json <span class="hljs-comment"># Instagram</span>
chatpack dc export.json    <span class="hljs-comment"># Discord</span>
</code></pre>
<p>I started with Telegram work groups. Then I added other messengers - to automate my processes and identify common trends.</p>
<hr />
<h2 id="heading-why-csv-compresses-best">Why CSV Compresses Best</h2>
<p>Comparison on real data (34K messages):</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Format</td><td>Tokens</td><td>Compression</td></tr>
</thead>
<tbody>
<tr>
<td>Raw JSON</td><td>11,177,258</td><td>baseline</td></tr>
<tr>
<td><strong>CSV</strong></td><td>849,915</td><td><strong>13.2x</strong></td></tr>
<tr>
<td>JSONL</td><td>1,029,130</td><td>10.9x</td></tr>
<tr>
<td>JSON (clean)</td><td>1,333,586</td><td>8.4x</td></tr>
</tbody>
</table>
</div><p>Why such a difference?</p>
<pre><code class="lang-json"><span class="hljs-comment">// JSON: each message is</span>
{<span class="hljs-attr">"sender"</span>: <span class="hljs-string">"Alice"</span>, <span class="hljs-attr">"content"</span>: <span class="hljs-string">"Hello"</span>}

<span class="hljs-comment">// CSV: just data</span>
Alice;Hello
</code></pre>
<p>JSON wastes tokens on:</p>
<ul>
<li><p>Brackets <code>{}</code> and <code>[]</code></p>
</li>
<li><p>Keys <code>"sender":</code>, <code>"content":</code> - for every message</p>
</li>
<li><p>Quotes around every string</p>
</li>
</ul>
<p>CSV uses <code>;</code> as a delimiter (one character) and writes keys only in the header.</p>
<p>You can also work with CSV in Excel - calculate response times, average message length, build engagement charts.</p>
<hr />
<h2 id="heading-merge-consecutive-the-obvious-feature-everyone-ignores">Merge Consecutive: The Obvious Feature Everyone Ignores</h2>
<p>A typical chat looks like this:</p>
<pre><code class="lang-plaintext">Alice: Hey
Alice: How are you?
Alice: Did you see the new project?
Bob: Yeah, I looked
Bob: Pretty good overall
</code></pre>
<p>After merge:</p>
<pre><code class="lang-plaintext">Alice: Hey
How are you?
Did you see the new project?
Bob: Yeah, I looked
Pretty good overall
</code></pre>
<p>Here's what the actual CSV output looks like:</p>
<pre><code class="lang-plaintext"># Without merge (5 rows, ~45 tokens)
Sender;Content
Alice;Hey
Alice;How are you?
Alice;Did you see the new project?
Bob;Yeah, I looked
Bob;Pretty good overall

# With merge (2 rows, ~35 tokens)
Sender;Content
Alice;Hey
How are you?
Did you see the new project?
Bob;Yeah, I looked
Pretty good overall
</code></pre>
<p><strong>5 messages → 2 entries.</strong> On 34K messages, this gives 24% reduction before even choosing a format.</p>
<p>But the main benefit isn't tokens. Embedding a single complete thought is processed much better than 5 fragmented pieces of the same thought. For RAG pipelines, this is critical.</p>
<p>Anyone who has worked with large chats eventually arrives at merge. This isn't an insight - it's common knowledge among those who analyze conversations.</p>
<hr />
<h2 id="heading-why-not-just-use-jq-or-a-python-script">Why Not Just Use jq or a Python Script?</h2>
<p>You could write <code>jq '.messages[] | {from, text}'</code>, but that won't give you merge, date filters, or fix Mojibake in Instagram exports. chatpack handles all the edge cases so you don't have to.</p>
<hr />
<h2 id="heading-architecture-why-traits-not-if-else">Architecture: Why Traits, Not If-Else</h2>
<p>When I wrote the Telegram parser, I built in extensibility from the start:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">ChatParser</span></span>: <span class="hljs-built_in">Send</span> + <span class="hljs-built_in">Sync</span> {
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">name</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; &amp;<span class="hljs-symbol">'static</span> <span class="hljs-built_in">str</span>;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">parse</span></span>(&amp;<span class="hljs-keyword">self</span>, file_path: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">Vec</span>&lt;InternalMessage&gt;, <span class="hljs-built_in">Box</span>&lt;<span class="hljs-keyword">dyn</span> Error&gt;&gt;;
}
</code></pre>
<p>Adding a new messenger - three steps:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// 1. Implement the parser</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">SignalParser</span></span>;
<span class="hljs-keyword">impl</span> ChatParser <span class="hljs-keyword">for</span> SignalParser {
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">name</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; &amp;<span class="hljs-symbol">'static</span> <span class="hljs-built_in">str</span> { <span class="hljs-string">"Signal"</span> }
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">parse</span></span>(&amp;<span class="hljs-keyword">self</span>, path: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">Vec</span>&lt;InternalMessage&gt;, _&gt; {
        <span class="hljs-comment">// ...</span>
    }
}

<span class="hljs-comment">// 2. Add to Source enum</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Source</span></span> { Telegram, WhatsApp, Instagram, Discord, Signal }

<span class="hljs-comment">// 3. Add to factory</span>
Source::Signal =&gt; <span class="hljs-built_in">Box</span>::new(SignalParser::new())
</code></pre>
<p>This was a planned decision from day one. The Strategy pattern isn't overengineering when you know you'll be extending.</p>
<hr />
<h2 id="heading-practical-use-cases">Practical Use Cases</h2>
<h3 id="heading-engagement-analysis">Engagement Analysis</h3>
<p>I needed to add an engagement metric - to identify when a person is most active and how this changed over the years.</p>
<pre><code class="lang-bash">chatpack tg group.json -t -o timeline.csv
</code></pre>
<p>The <code>-t</code> flag adds timestamps. Now the CSV has a date column for each message:</p>
<pre><code class="lang-plaintext">Timestamp;Sender;Content
2024-01-15 10:30:00;Alice;Good morning!
2024-01-15 10:31:00;Bob;Hey
</code></pre>
<p>Upload to Claude: <em>"Build an activity chart by hour for each participant."</em></p>
<p>An LLM can't understand what someone is doing outside the chat. But general trends - when the platform changed, who was active during which period, how team mood shifted - it identifies perfectly.</p>
<h3 id="heading-hr-patterns">HR Patterns</h3>
<p>I use filters often when I need to identify patterns for a specific person:</p>
<pre><code class="lang-bash">chatpack tg chat.json --from <span class="hljs-string">"Ivan"</span> -o ivan.csv
</code></pre>
<p>I get all messages from one participant. Then analyze:</p>
<ul>
<li><p>Grammar</p>
</li>
<li><p>Humor style</p>
</li>
<li><p>Average message length</p>
</li>
<li><p>Activity times</p>
</li>
</ul>
<p>It's like HR screening before team selection - just automated.</p>
<h3 id="heading-project-archaeology">Project Archaeology</h3>
<p>The most useful case for me personally:</p>
<pre><code class="lang-bash">chatpack tg chat.json --after 2022-01-01 --before 2023-01-01 -o year_2022.csv
</code></pre>
<p>I was looking for information about the old platform, former staff and students, advice from graduates about where they got jobs. The LLM handled the task, identifying what I needed. Over time, I formulated concrete ideas for improving the platform.</p>
<hr />
<h2 id="heading-technical-details">Technical Details</h2>
<h3 id="heading-performance">Performance</h3>
<ul>
<li><p><strong>Speed:</strong> 20-50K messages/sec</p>
</li>
<li><p><strong>Memory:</strong> ~3x file size (entire JSON in memory)</p>
</li>
<li><p><strong>Ceiling:</strong> files up to 500MB comfortably, 1GB+ needs streaming (not implemented)</p>
</li>
</ul>
<p>Why no streaming? I just needed a library that would solve my problem. Most of the time, message exports don't exceed 1 MB. I consciously chose not to overcomplicate things.</p>
<h3 id="heading-when-chatpack-wont-help">When chatpack Won't Help</h3>
<ul>
<li><p><strong>Files &gt;1GB</strong> - no streaming, will eat your RAM</p>
</li>
<li><p><strong>Media context needed</strong> - photos, voice messages are ignored, text only</p>
</li>
<li><p><strong>Real-time analysis</strong> - this is a batch tool</p>
</li>
<li><p><strong>Preserving exact JSON structure</strong> - chatpack normalizes everything to a flat format</p>
</li>
</ul>
<h3 id="heading-why-rust">Why Rust</h3>
<p>Rust is currently my main stack. I used it because the project fits the criteria:</p>
<ul>
<li><p>CLI tool - Rust is ideal</p>
</li>
<li><p>Need speed - check</p>
</li>
<li><p>Need reliability - types prevent mistakes</p>
</li>
</ul>
<p>Thanks to this project, I also learned how to work with WASM and built a web version.</p>
<h3 id="heading-handling-edge-cases">Handling Edge Cases</h3>
<p>WhatsApp exports dates in different formats depending on locale:</p>
<pre><code class="lang-plaintext">[1/15/24, 10:30 AM]      // US
[15.01.24, 10:30]        // EU
26.10.2025, 20:40 -      // RU
</code></pre>
<p>chatpack auto-detects the format from the first 20 lines and parses correctly.</p>
<p>Instagram stores UTF-8 text as ISO-8859-1 (Mojibake). "Привет" becomes "ÐŸÑ€Ð¸Ð²ÐµÑ‚". The parser fixes this automatically.</p>
<hr />
<h2 id="heading-wasm-chatpack-in-the-browser">WASM: chatpack in the Browser</h2>
<p>Not everyone wants to install a CLI. Some people just need to convert one file without touching the terminal. That's why there's <a target="_blank" href="https://chatpack.berektassuly.com">chatpack.berektassuly.com</a>.</p>
<p>The workflow is simple:</p>
<ol>
<li><p>Drag &amp; drop your export file</p>
</li>
<li><p>Choose output format</p>
</li>
<li><p>Download the result</p>
</li>
</ol>
<p>The key point: <strong>your file never leaves your machine</strong>. Everything is processed locally in the browser via WebAssembly. No server, no uploads, no privacy concerns.</p>
<p>Repository: <a target="_blank" href="https://github.com/Berektassuly/chatpack-web">github.com/Berektassuly/chatpack-web</a></p>
<hr />
<h2 id="heading-installation-and-usage">Installation and Usage</h2>
<h3 id="heading-pre-built-binaries">Pre-built Binaries</h3>
<p>Download for your platform from <a target="_blank" href="https://github.com/berektassuly/chatpack/releases/latest">GitHub Releases</a>:</p>
<ul>
<li><p>Windows (x64)</p>
</li>
<li><p>macOS (Intel and Apple Silicon)</p>
</li>
<li><p>Linux (x64)</p>
</li>
</ul>
<h3 id="heading-via-cargo">Via Cargo</h3>
<pre><code class="lang-bash">cargo install chatpack
</code></pre>
<h3 id="heading-as-a-library">As a Library</h3>
<pre><code class="lang-toml"><span class="hljs-section">[dependencies]</span>
<span class="hljs-attr">chatpack</span> = <span class="hljs-string">"0.3"</span>
</code></pre>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> chatpack::prelude::*;

<span class="hljs-keyword">let</span> parser = create_parser(Source::Telegram);
<span class="hljs-keyword">let</span> messages = parser.parse(<span class="hljs-string">"chat.json"</span>)?;
<span class="hljs-keyword">let</span> merged = merge_consecutive(messages);
write_csv(&amp;merged, <span class="hljs-string">"output.csv"</span>, &amp;OutputConfig::new())?;
</code></pre>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>chatpack is a tool I wrote for myself. The problem was specific: analyze chat history through an LLM. The solution turned out to be universal.</p>
<p>If I were starting today, I'd keep the development process the same:</p>
<ol>
<li><p>Telegram parser</p>
</li>
<li><p>Trait for extensibility</p>
</li>
<li><p>Other messengers as needed</p>
</li>
<li><p>WASM for those who find CLI overkill</p>
</li>
</ol>
<p>If anyone needs it - feel free to use it in your project. If there are issues, I'll listen to what users need and fix it.</p>
<hr />
<p><strong>Links:</strong></p>
<ul>
<li><p>GitHub: <a target="_blank" href="https://github.com/berektassuly/chatpack">github.com/berektassuly/chatpack</a></p>
</li>
<li><p>Web: <a target="_blank" href="https://chatpack.berektassuly.com">chatpack.berektassuly.com</a></p>
</li>
<li><p>WASM repo: <a target="_blank" href="https://github.com/Berektassuly/chatpack-web">github.com/Berektassuly/chatpack-web</a></p>
</li>
<li><p>Author: <a target="_blank" href="https://www.linkedin.com/in/mukhammedali-berektassuly/">Mukhammedali Berektassuly</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Architecture as LEGO: Building a Testable Rust Service with Blockchain Abstraction]]></title><description><![CDATA[Don't let blockchain slow down your CI/CD. Learn how to use Rust traits, Arc, and Dependency Injection to decouple business logic from Solana for millisecond unit tests.
Imagine you're building a service for issuing digital diplomas that records docu...]]></description><link>https://berektassuly.com/architecture-as-lego-rust-testing</link><guid isPermaLink="true">https://berektassuly.com/architecture-as-lego-rust-testing</guid><dc:creator><![CDATA[Mukhammedali Berektassuly]]></dc:creator><pubDate>Tue, 25 Nov 2025 18:09:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764095061314/6b43cf06-779f-47c4-b9f7-1b2503787124.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-dont-let-blockchain-slow-down-your-cicd-learn-how-to-use-rust-traits-arc-and-dependency-injection-to-decouple-business-logic-from-solana-for-millisecond-unit-tests">Don't let blockchain slow down your CI/CD. Learn how to use Rust traits, Arc, and Dependency Injection to decouple business logic from Solana for millisecond unit tests.</h1>
<p>Imagine you're building a service for issuing digital diplomas that records document hashes on the Solana blockchain. Everything works great until you try to write your first unit test. Suddenly, you realize that testing simple business logic requires spinning up a local Solana validator, obtaining test tokens, and praying the network doesn't crash in the middle of your CI/CD pipeline. And what happens when your client asks for Ethereum support tomorrow? Rewrite half your codebase?</p>
<p>In this article, I'll show you how we solved this problem in a real production project using the power of Rust's type system and the Strategy pattern. You'll learn how to properly leverage async-trait, why <code>dyn Trait</code> sometimes beats generics, and how to write tests that run in milliseconds instead of minutes.</p>
<h2 id="heading-the-anti-pattern-how-we-suffered-before-refactoring">The Anti-Pattern: How We Suffered BEFORE Refactoring</h2>
<p>Let's be honest - our first version looked something like this:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// ❌ BAD: Direct dependency on Solana everywhere</span>
<span class="hljs-keyword">use</span> solana_client::rpc_client::RpcClient;
<span class="hljs-keyword">use</span> solana_sdk::{signature::Keypair, transaction::Transaction};

<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AppState</span></span> {
    <span class="hljs-keyword">pub</span> solana_client: RpcClient,  <span class="hljs-comment">// &lt;- Concrete implementation!</span>
    <span class="hljs-keyword">pub</span> keypair: Keypair,
    <span class="hljs-keyword">pub</span> db_client: Postgrest,
}

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">issue_diploma</span></span>(
    State(state): State&lt;Arc&lt;AppState&gt;&gt;,
    <span class="hljs-comment">// ...</span>
) -&gt; <span class="hljs-built_in">Result</span>&lt;Json&lt;IssueResponse&gt;, AppError&gt; {
    <span class="hljs-comment">// Business logic tightly coupled to Solana</span>
    <span class="hljs-keyword">let</span> instruction = <span class="hljs-comment">/* create Solana-specific instruction */</span>;
    <span class="hljs-keyword">let</span> transaction = Transaction::new(<span class="hljs-comment">/* ... */</span>);

    <span class="hljs-comment">// Direct Solana RPC call</span>
    <span class="hljs-keyword">let</span> signature = state.solana_client
        .send_and_confirm_transaction(&amp;transaction)?;

    <span class="hljs-comment">// Now try testing this...</span>
}

<span class="hljs-meta">#[cfg(test)]</span>
<span class="hljs-keyword">mod</span> tests {
    <span class="hljs-meta">#[tokio::test]</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">test_issue_diploma</span></span>() {
        <span class="hljs-comment">// 😱 Test needs real Solana!</span>
        <span class="hljs-comment">// Options:</span>
        <span class="hljs-comment">// 1. Spin up solana-test-validator (slow)</span>
        <span class="hljs-comment">// 2. Use devnet (unstable)</span>
        <span class="hljs-comment">// 3. Hardcode mocks... everywhere (maintenance nightmare)</span>

        <span class="hljs-comment">// Result: test is either slow, brittle, or both</span>
    }
}
</code></pre>
<p><strong>What's wrong here?</strong></p>
<ul>
<li><p>🔒 <strong>Vendor lock-in</strong>: Want to switch to Ethereum? Good luck rewriting everything</p>
</li>
<li><p>🐌 <strong>Slow tests</strong>: Each test waits for real transactions (30-60 seconds)</p>
</li>
<li><p>💸 <strong>Expensive tests</strong>: Devnet requires test SOL, has rate limits</p>
</li>
<li><p>🎲 <strong>Unstable CI/CD</strong>: Network down? All tests red</p>
</li>
<li><p>🍝 <strong>Spaghetti code</strong>: Business logic tangled with blockchain details</p>
</li>
</ul>
<h2 id="heading-the-problem-when-blockchain-becomes-an-anchor">The Problem: When Blockchain Becomes an Anchor</h2>
<p>While developing our diploma verification service, we hit the classic tight coupling problem. Our code directly depended on the Solana RPC client, creating a whole bouquet of issues:</p>
<ul>
<li><p><strong>Masochistic tests</strong>: Every test required a real blockchain connection</p>
</li>
<li><p><strong>Slow development</strong>: Waiting for transaction confirmations on every test run is a special kind of hell</p>
</li>
<li><p><strong>Brittle CI/CD</strong>: Tests failed due to network issues, not actual bugs</p>
</li>
<li><p><strong>Vendor lock-in</strong>: Switching to another blockchain meant rewriting most of the service</p>
</li>
</ul>
<h2 id="heading-solution-architecture-a-visual-guide">Solution Architecture: A Visual Guide</h2>
<p>Before diving into code, let's look at the big picture of our architecture:</p>
<pre><code class="lang-mermaid">graph TD
    HTTP[HTTP Requests] --&gt; AxumRouter

    %% =====================
    %% Axum Router
    %% =====================
    subgraph AxumRouter [Axum Router]
        direction TB
        Issue[/issue/]:::router
        Verify[/verify/]:::router
        Creds[/credentials/]:::router
    end

    AxumRouter --&gt; AppState

    %% =====================
    %% AppState
    %% =====================
    subgraph AppState
        direction TB
        ChainClient["chain_client:
Arc(dyn ChainClient)"]
        IssuerKey["issuer_keypair:
Keypair"]
        DBClient["db_client:
Postgrest"]
    end

    AppState --&gt;|implements| ChainClientTrait

    %% =====================
    %% ChainClient trait
    %% =====================
    subgraph ChainClientTrait ["trait ChainClient"]
        direction TB
        WriteHash["async write_hash()
-&gt; Result"]
        FindByHash["async find_by_hash()
-&gt; Result"]
        HealthCheck["async health_check()
-&gt; Result"]
    end

    ChainClientTrait --&gt; SolanaClient
    ChainClientTrait --&gt; MockClient
    ChainClientTrait --&gt; EthereumClient

    %% =====================
    %% Implementations
    %% =====================
    subgraph Production
        SolanaClient["SolanaClient
Real RPC
Real txs
Real fees"]
    end

    subgraph Testing
        MockClient["MockClient
HashMap
Instant
No network"]
    end

    subgraph Future
        EthereumClient["EthereumClient
(easily added)"]
    end

    %% =====================
    %% External systems
    %% =====================
    SolanaClient --&gt; SolanaBlockchain["Solana Blockchain"]
    MockClient --&gt; InMemory["In-memory storage"]

    %% =====================
    %% Styles
    %% =====================
    classDef router fill:#f0f0f0,stroke:#333,stroke-width:1px
</code></pre>
<h3 id="heading-data-flow-in-different-environments">Data Flow in Different Environments:</h3>
<pre><code class="lang-mermaid">graph BT
    subgraph TESTING
        T1[Test] --&gt; T2[Handler]
        T2 --&gt; T3[AppState]
        T3 --&gt; T4[MockClient]
        T4 --&gt; T5[HashMap in Memory]
        T3 -.-&gt; T6["(dyn ChainClient)"]
    end

    subgraph PRODUCTION
        P1[Request] --&gt; P2[Handler]
        P2 --&gt; P3[AppState]
        P3 --&gt; P4[SolanaClient]
        P4 --&gt; P5[Solana Network]
        P3 -.-&gt; P6["(dyn ChainClient)"]
    end

    style PRODUCTION fill:#e1f5ff
    style TESTING fill:#fff4e1
    style P6 fill:#ffebee
    style T6 fill:#ffebee
</code></pre>
<h2 id="heading-the-solution-chainclient-trait-as-our-contract-with-the-blockchain">The Solution: ChainClient Trait as Our Contract with the Blockchain</h2>
<p>Instead of dragging a concrete Solana client implementation everywhere, we created an abstraction - a trait that describes <em>what</em> any blockchain client should be able to do, without specifying <em>how</em>:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// ./internal/blockchain/mod.rs</span>
<span class="hljs-keyword">use</span> async_trait::async_trait;

<span class="hljs-meta">#[async_trait]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">ChainClient</span></span>: <span class="hljs-built_in">Send</span> + <span class="hljs-built_in">Sync</span> {
    <span class="hljs-comment">/// Writes a hash to the blockchain.</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_hash</span></span>(&amp;<span class="hljs-keyword">self</span>, hash: &amp;<span class="hljs-built_in">str</span>, meta: &amp;Diploma)
        -&gt; <span class="hljs-built_in">Result</span>&lt;BlockchainRecord, AppError&gt;;

    <span class="hljs-comment">/// Looks up a hash on the blockchain.</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">find_by_hash</span></span>(&amp;<span class="hljs-keyword">self</span>, hash: &amp;<span class="hljs-built_in">str</span>) 
        -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">Option</span>&lt;BlockchainRecord&gt;, AppError&gt;;

    <span class="hljs-comment">/// Performs a health check on the connection.</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">health_check</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;(), AppError&gt;;
}
</code></pre>
<h3 id="heading-why-asynctrait">Why async_trait?</h3>
<p>Notice the <code>#[async_trait]</code> macro? This isn't a whim - it's a necessity. At the time of writing, async functions in traits are still not stabilized in Rust (though work is progressing rapidly). The <code>async-trait</code> library solves this elegantly: under the hood, it transforms:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_hash</span></span>(&amp;<span class="hljs-keyword">self</span>, ...) -&gt; <span class="hljs-built_in">Result</span>&lt;...&gt;
</code></pre>
<p>Into something like:</p>
<pre><code class="lang-rust"><span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_hash</span></span>(&amp;<span class="hljs-keyword">self</span>, ...) -&gt; <span class="hljs-built_in">Box</span>&lt;<span class="hljs-keyword">dyn</span> Future&lt;Output = <span class="hljs-built_in">Result</span>&lt;...&gt;&gt; + <span class="hljs-built_in">Send</span>&gt;
</code></pre>
<p>Yes, this adds a small overhead due to dynamic dispatch and heap allocation, but for I/O operations (and blockchain work is always I/O), it's absolutely negligible.</p>
<h2 id="heading-production-implementation-solanachainclient">Production Implementation: SolanaChainClient</h2>
<p>Now let's look at the concrete implementation for Solana. It encapsulates all the complexity of working with RPC and transactions:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// ./internal/blockchain/solana.rs</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">SolanaChainClient</span></span> {
    rpc_client: RpcClient,
    issuer_keypair: Keypair,
}

<span class="hljs-keyword">impl</span> SolanaChainClient {
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">new</span></span>(rpc_url: <span class="hljs-built_in">String</span>, issuer_keypair: Keypair) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-keyword">Self</span>, AppError&gt; {
        <span class="hljs-keyword">let</span> rpc_client = RpcClient::new(rpc_url);
        <span class="hljs-literal">Ok</span>(<span class="hljs-keyword">Self</span> {
            rpc_client,
            issuer_keypair,
        })
    }
}

<span class="hljs-meta">#[async_trait]</span>
<span class="hljs-keyword">impl</span> ChainClient <span class="hljs-keyword">for</span> SolanaChainClient {
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_hash</span></span>(
        &amp;<span class="hljs-keyword">self</span>,
        hash: &amp;<span class="hljs-built_in">str</span>,
        _meta: &amp;Diploma,
    ) -&gt; <span class="hljs-built_in">Result</span>&lt;BlockchainRecord, AppError&gt; {
        <span class="hljs-comment">// Using Memo Program to record the hash</span>
        <span class="hljs-keyword">let</span> memo_program_id = 
            Pubkey::from_str(<span class="hljs-string">"MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"</span>)?;

        <span class="hljs-keyword">let</span> instruction = Instruction::new_with_bytes(
            memo_program_id,
            hash.as_bytes(),
            <span class="hljs-built_in">vec!</span>[],
        );

        <span class="hljs-comment">// Get latest blockhash and send transaction</span>
        <span class="hljs-keyword">let</span> latest_blockhash = <span class="hljs-keyword">self</span>.rpc_client.get_latest_blockhash()?;
        <span class="hljs-keyword">let</span> message = Message::new(&amp;[instruction], <span class="hljs-literal">Some</span>(&amp;<span class="hljs-keyword">self</span>.issuer_keypair.pubkey()));
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> transaction = Transaction::new_unsigned(message);

        transaction.sign(&amp;[&amp;<span class="hljs-keyword">self</span>.issuer_keypair], latest_blockhash);

        <span class="hljs-keyword">let</span> signature = <span class="hljs-keyword">self</span>.rpc_client
            .send_and_confirm_transaction(&amp;transaction)?;

        <span class="hljs-literal">Ok</span>(BlockchainRecord {
            tx_id: signature.to_string(),
            block_time: <span class="hljs-literal">None</span>,
            raw_meta: <span class="hljs-literal">Some</span>(hash.to_string()),
        })
    }

    <span class="hljs-comment">// Other methods...</span>
}
</code></pre>
<p>All the Solana magic (working with instructions, signatures, blockhashes) is hidden inside the implementation. For the rest of the code, it's just "something that can write hashes to a blockchain."</p>
<h2 id="heading-mockchainclient-a-developers-testing-paradise">MockChainClient: A Developer's Testing Paradise</h2>
<p>And here's where the real magic begins - our mock implementation for tests:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// ./internal/blockchain/mock.rs</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">MockChainClient</span></span> {
    storage: Mutex&lt;HashMap&lt;<span class="hljs-built_in">String</span>, BlockchainRecord&gt;&gt;,
}

<span class="hljs-keyword">impl</span> MockChainClient {
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">new</span></span>() -&gt; <span class="hljs-keyword">Self</span> {
        <span class="hljs-keyword">Self</span> {
            storage: Mutex::new(HashMap::new()),
        }
    }
}

<span class="hljs-meta">#[async_trait]</span>
<span class="hljs-keyword">impl</span> ChainClient <span class="hljs-keyword">for</span> MockChainClient {
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_hash</span></span>(
        &amp;<span class="hljs-keyword">self</span>,
        hash: &amp;<span class="hljs-built_in">str</span>,
        _meta: &amp;Diploma,
    ) -&gt; <span class="hljs-built_in">Result</span>&lt;BlockchainRecord, AppError&gt; {
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> storage = <span class="hljs-keyword">self</span>.storage.lock().unwrap();
        <span class="hljs-keyword">let</span> record = BlockchainRecord {
            tx_id: <span class="hljs-built_in">format!</span>(<span class="hljs-string">"mock_tx_{}"</span>, hash),
            block_time: <span class="hljs-literal">Some</span>(Utc::now()),
            raw_meta: <span class="hljs-literal">Some</span>(hash.to_string()),
        };
        storage.insert(hash.to_string(), record.clone());
        <span class="hljs-literal">Ok</span>(record)
    }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">find_by_hash</span></span>(&amp;<span class="hljs-keyword">self</span>, hash: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">Option</span>&lt;BlockchainRecord&gt;, AppError&gt; {
        <span class="hljs-keyword">let</span> storage = <span class="hljs-keyword">self</span>.storage.lock().unwrap();
        <span class="hljs-literal">Ok</span>(storage.get(hash).cloned())
    }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">health_check</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;(), AppError&gt; {
        <span class="hljs-literal">Ok</span>(()) <span class="hljs-comment">// Always healthy!</span>
    }
}
</code></pre>
<p>Instead of a real blockchain, we use a simple in-memory <code>HashMap</code>. Transactions are "confirmed" instantly, no network delays, no fees. Now we can write a test for the <code>issue_diploma</code> handler that runs in milliseconds:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[cfg(test)]</span>
<span class="hljs-keyword">mod</span> tests {
    <span class="hljs-keyword">use</span> super::*;

    <span class="hljs-meta">#[tokio::test]</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">test_issue_diploma_creates_blockchain_record</span></span>() {
        <span class="hljs-comment">// Arrange: create mock client instead of real one</span>
        <span class="hljs-keyword">let</span> mock_client = Arc::new(MockChainClient::new());
        <span class="hljs-keyword">let</span> app_state = Arc::new(AppState {
            chain_client: mock_client.clone(),
            <span class="hljs-comment">// ... other fields</span>
        });

        <span class="hljs-comment">// Act: call business logic</span>
        <span class="hljs-keyword">let</span> result = issue_diploma(State(app_state), test_multipart).<span class="hljs-keyword">await</span>;

        <span class="hljs-comment">// Assert: verify hash was written</span>
        <span class="hljs-built_in">assert!</span>(result.is_ok());
        <span class="hljs-keyword">let</span> response = result.unwrap();

        <span class="hljs-comment">// We can even verify the hash was actually saved</span>
        <span class="hljs-keyword">let</span> record = mock_client.find_by_hash(&amp;response.hash).<span class="hljs-keyword">await</span>.unwrap();
        <span class="hljs-built_in">assert!</span>(record.is_some());
        <span class="hljs-built_in">assert_eq!</span>(record.unwrap().tx_id, <span class="hljs-built_in">format!</span>(<span class="hljs-string">"mock_tx_{}"</span>, response.hash));
    }
}
</code></pre>
<h2 id="heading-dependency-injection-via-appstate">Dependency Injection via AppState</h2>
<p>Now for the interesting part - how it all comes together in a real application. We're using Axum (an excellent web framework for Rust), and all the magic happens in <code>AppState</code>:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// ./internal/api/router.rs</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AppState</span></span> {
    <span class="hljs-keyword">pub</span> chain_client: Arc&lt;<span class="hljs-keyword">dyn</span> ChainClient&gt;,  <span class="hljs-comment">// &lt;- There it is!</span>
    <span class="hljs-keyword">pub</span> issuer_keypair: Keypair,
    <span class="hljs-keyword">pub</span> db_client: Postgrest,
}

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">create_router</span></span>() -&gt; <span class="hljs-built_in">Result</span>&lt;Router, AppError&gt; {
    <span class="hljs-comment">// Load configuration</span>
    <span class="hljs-keyword">let</span> config = Config::from_env()?;

    <span class="hljs-comment">// Create concrete implementation for production</span>
    <span class="hljs-keyword">let</span> solana_client = SolanaChainClient::new(
        config.solana_rpc_url, 
        client_keypair
    )?;

    <span class="hljs-comment">// Package into AppState</span>
    <span class="hljs-keyword">let</span> app_state = Arc::new(AppState {
        chain_client: Arc::new(solana_client), <span class="hljs-comment">// &lt;- Dependency injection!</span>
        issuer_keypair,
        db_client,
    });

    <span class="hljs-comment">// Create router with injected state</span>
    <span class="hljs-literal">Ok</span>(Router::new()
        .route(<span class="hljs-string">"/issue"</span>, post(issue_diploma))
        .route(<span class="hljs-string">"/verify/:hash"</span>, get(verify_diploma))
        .with_state(app_state))
}
</code></pre>
<h3 id="heading-whats-dyn-chainclient">What's <code>dyn ChainClient</code>?</h3>
<p>Notice <code>Arc&lt;dyn ChainClient&gt;</code>? This is a trait object - a way to store any type implementing the <code>ChainClient</code> trait without knowing the concrete type at compile time.</p>
<ul>
<li><p><code>dyn</code> tells the compiler: "this will be some type implementing <code>ChainClient</code>, but which one exactly - we'll find out at runtime"</p>
</li>
<li><p><code>Arc</code> is needed for safe sharing between threads (Axum handles requests in parallel)</p>
</li>
</ul>
<h2 id="heading-dyn-trait-vs-generics-battle-of-the-titans">dyn Trait vs Generics: Battle of the Titans</h2>
<p>You might ask: "Why not use generics?" Excellent question! Let's compare:</p>
<h3 id="heading-option-with-generics-static-dispatch">Option with Generics (Static Dispatch):</h3>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AppState</span></span>&lt;C: ChainClient&gt; {
    <span class="hljs-keyword">pub</span> chain_client: Arc&lt;C&gt;,
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Maximum performance (compiler can inline calls)</p>
</li>
<li><p>No vtable lookup overhead</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>Monomorphization bloats the binary (separate code copy generated for each type <code>C</code>)</p>
</li>
<li><p>Can't change implementation at runtime</p>
</li>
<li><p>All handlers must also become generic functions</p>
</li>
</ul>
<h3 id="heading-our-choice-trait-objects-dynamic-dispatch">Our Choice: Trait Objects (Dynamic Dispatch):</h3>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">AppState</span></span> {
    <span class="hljs-keyword">pub</span> chain_client: Arc&lt;<span class="hljs-keyword">dyn</span> ChainClient&gt;,
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p><strong>Pros:</strong></p>
<ul>
<li><p>Flexibility: can choose implementation at runtime (e.g., based on environment variable)</p>
</li>
<li><p>Smaller binary size</p>
</li>
<li><p>Easier to integrate with web frameworks</p>
</li>
</ul>
<p><strong>Cons:</strong></p>
<ul>
<li><p>Small overhead for calls (vtable lookup)</p>
</li>
<li><p>Need <code>Arc</code> to work with trait objects</p>
</li>
</ul>
<p>For our use case, the choice is clear: the overhead of vtable lookup is negligible compared to network call times to the blockchain. And the flexibility we gain is priceless.</p>
<h2 id="heading-conclusion-architecture-that-evolves-with-you">Conclusion: Architecture That Evolves With You</h2>
<p>What we achieved:</p>
<p><strong>Lightning-fast tests</strong>: Unit tests run in milliseconds without requiring a real blockchain</p>
<p><strong>Easy blockchain swapping</strong>: Want to add Ethereum? Just write <code>EthereumChainClient</code> implementing <code>ChainClient</code></p>
<p><strong>Clear separation of concerns</strong>: Business logic knows nothing about Solana implementation details</p>
<p><strong>Simplified debugging</strong>: Can use <code>MockChainClient</code> even in development environment for rapid iteration</p>
<p>This approach isn't a silver bullet, but for services interacting with external systems (whether blockchains, payment systems, or third-party APIs), it provides enormous flexibility and testability.</p>
<p>Remember: good architecture isn't one that anticipates all future changes, but one that makes them easy to implement when needed. Rust traits give us exactly this capability.</p>
<p><strong>P.S.</strong> If you're still writing tests that require real connections to external services - give this approach a try. Your colleagues (and your CI/CD pipeline) will thank you!</p>
]]></content:encoded></item><item><title><![CDATA[Solana, PostgreSQL, and the Perils of Dual-Writes: A Rust Case Study]]></title><description><![CDATA[When Blockchain Meets PostgreSQL: Solving the Dual-Write Dilemma in Our Rust Service
Introduction: Two Sources of Truth, One Big Headache
Picture this: you're building a diploma verification system. The requirements seem straightforward—data must be ...]]></description><link>https://berektassuly.com/solana-postgresql-dual-write-rust-case-study</link><guid isPermaLink="true">https://berektassuly.com/solana-postgresql-dual-write-rust-case-study</guid><dc:creator><![CDATA[Mukhammedali Berektassuly]]></dc:creator><pubDate>Wed, 19 Nov 2025 04:39:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1763527615617/18218576-af1d-4900-bbff-784c3c56a1fd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-when-blockchain-meets-postgresql-solving-the-dual-write-dilemma-in-our-rust-service">When Blockchain Meets PostgreSQL: Solving the Dual-Write Dilemma in Our Rust Service</h1>
<h2 id="heading-introduction-two-sources-of-truth-one-big-headache">Introduction: Two Sources of Truth, One Big Headache</h2>
<p>Picture this: you're building a diploma verification system. The requirements seem straightforward—data must be immutable (hello, blockchain) while remaining queryable at speed (hello, PostgreSQL). The obvious solution? Write to both. But as every battle-scarred engineer knows, the devil lurks in the implementation details.</p>
<p>Our project uses the dual-write pattern:</p>
<ul>
<li><p><strong>Solana</strong> — guarantees immutability and transparency for issued diplomas</p>
</li>
<li><p><strong>PostgreSQL (Supabase)</strong> — enables fast queries and complex analytics</p>
</li>
</ul>
<p>Looks great on architecture diagrams, but production tells a different story. The killer issue? Partial failures. Your Solana transaction succeeds, the diploma is forever etched into the blockchain, but then PostgreSQL decides to take a coffee break. The user gets their confirmation, but half your system has no idea their diploma exists.</p>
<p>Today I'll walk you through how we faced this beast head-on and the patterns we deployed to tame it.</p>
<h2 id="heading-anatomy-of-a-failure-where-things-break">Anatomy of a Failure: Where Things Break</h2>
<p>Let's dive into the actual code from our <code>internal/api/handlers.rs</code>. The <code>issue_diploma</code> function is where the magic happens... and where everything can go sideways:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">issue_diploma</span></span>(
    State(state): State&lt;Arc&lt;AppState&gt;&gt;,
    <span class="hljs-keyword">mut</span> multipart: Multipart,
) -&gt; <span class="hljs-built_in">Result</span>&lt;Json&lt;IssueResponse&gt;, AppError&gt; {
    <span class="hljs-comment">// ... multipart data parsing ...</span>

    <span class="hljs-comment">// Generate diploma hash</span>
    <span class="hljs-keyword">let</span> hash = hashing::generate_hash(
        &amp;file_bytes,
        &amp;req.issuer_id,
        &amp;req.recipient_id,
        issued_at,
        req.serial.as_deref(),
    );

    <span class="hljs-comment">// Sign the hash with private key</span>
    <span class="hljs-keyword">let</span> signature = hashing::sign_hash(&amp;hash, &amp;state.issuer_keypair)?;

    <span class="hljs-keyword">let</span> diploma = Diploma {
        hash: hash.clone(),
        issuer_id: req.issuer_id.clone(),
        recipient_id: req.recipient_id.clone(),
        signature: <span class="hljs-literal">Some</span>(signature.clone()),
        issued_at,
        serial: req.serial.clone(),
        ipfs_cid: <span class="hljs-literal">None</span>,
    };

    <span class="hljs-comment">// CRITICAL POINT #1: Writing to Solana blockchain</span>
    <span class="hljs-comment">// This operation can take 1-3 seconds and costs money (gas)</span>
    <span class="hljs-keyword">let</span> chain_record = state.chain_client.write_hash(&amp;hash, &amp;diploma).<span class="hljs-keyword">await</span>?;

    <span class="hljs-comment">// Prepare data for database</span>
    <span class="hljs-keyword">let</span> credential_data = serde_json::json!({
        <span class="hljs-string">"hash"</span>: &amp;diploma.hash,
        <span class="hljs-string">"issuer_id"</span>: &amp;diploma.issuer_id,
        <span class="hljs-string">"recipient_id"</span>: &amp;diploma.recipient_id,
        <span class="hljs-string">"solana_tx_id"</span>: &amp;chain_record.tx_id,
        <span class="hljs-string">"issued_at"</span>: diploma.issued_at.to_rfc3339(),
    });

    <span class="hljs-comment">// CRITICAL POINT #2: Writing to PostgreSQL</span>
    <span class="hljs-comment">// And here's where disaster can strike</span>
    <span class="hljs-keyword">let</span> db_response = state
        .db_client
        .from(<span class="hljs-string">"credentials"</span>)
        .insert(credential_data.to_string())
        .execute()
        .<span class="hljs-keyword">await</span>;

    <span class="hljs-comment">// Handle database error</span>
    <span class="hljs-keyword">let</span> db_response = <span class="hljs-keyword">match</span> db_response {
        <span class="hljs-literal">Ok</span>(response) =&gt; response,
        <span class="hljs-literal">Err</span>(e) =&gt; {
            tracing::error!(<span class="hljs-string">"Database request failed: {}"</span>, e);
            <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(AppError::Database(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"Database request failed: {}"</span>, e)));
        }
    };

    <span class="hljs-keyword">if</span> !db_response.status().is_success() {
        <span class="hljs-keyword">let</span> status = db_response.status();
        <span class="hljs-keyword">let</span> error_body = db_response.text().<span class="hljs-keyword">await</span>.unwrap_or_default();

        <span class="hljs-comment">// HERE IT IS - THE CRITICAL INCONSISTENCY!</span>
        tracing::error!(
            <span class="hljs-string">"CRITICAL INCONSISTENCY: Failed to save to Supabase after successful Solana transaction. \
             tx_id: {}, hash: {}. Status: {}. Body: {}"</span>,
            chain_record.tx_id,
            hash,
            status,
            error_body
        );

        <span class="hljs-comment">// We've already written to blockchain, can't roll back!</span>
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(AppError::Internal(
            <span class="hljs-string">"Failed to save credential record after blockchain confirmation."</span>.to_string(),
        ));
    }

    <span class="hljs-comment">// If all is well - return success response</span>
    <span class="hljs-literal">Ok</span>(Json(IssueResponse {
        hash,
        tx_id: chain_record.tx_id,
        signature: <span class="hljs-literal">Some</span>(signature),
        issued_at,
    }))
}
</code></pre>
<p>Here's the sequence of operations and the failure point visualized:</p>
<pre><code class="lang-plaintext">┌──────────┐      ┌────────────┐     ┌─────────┐      ┌────────────┐
│  Client  │────▶│ Rust API   │───▶│ Solana  │────▶│  SUCCESS   │
└──────────┘      └────────────┘     └─────────┘      └────────────┘
                        │                                    │
                        │                                    ▼
                        │                             ┌────────────┐
                        └───────────────────────────▶│ PostgreSQL │
                                                      └────────────┘
                                                            │
                                                            ▼
                                                     ┌────────────┐
                                                     │   FAIL!    │
                                                     └────────────┘
                                                            │
                                                            ▼
                                                ┌──────────────────────┐
                                                │ DATA DRIFT:          │
                                                │ • Blockchain: ✓      │
                                                │ • Database: ✗        │
                                                └──────────────────────┘
</code></pre>
<h3 id="heading-the-fallout">The Fallout</h3>
<p>What happens after such a failure? Several unpleasant scenarios:</p>
<ol>
<li><p><strong>Users can't find their diploma</strong> through API queries to the database</p>
</li>
<li><p><strong>Analytics become impossible</strong> — database has incomplete data</p>
</li>
<li><p><strong>Audit nightmares</strong> — blockchain has the record, reports don't</p>
</li>
<li><p><strong>Duplication on retry</strong> — users might attempt to issue the diploma again</p>
</li>
</ol>
<h2 id="heading-theory-meets-practice-patterns-to-the-rescue">Theory Meets Practice: Patterns to the Rescue</h2>
<h3 id="heading-the-consistency-problem-choosing-a-strategy">The Consistency Problem: Choosing a Strategy</h3>
<p>In distributed systems, there are two main approaches to consistency:</p>
<ol>
<li><p><strong>Strong Consistency</strong> — all nodes see identical data at the same moment. This is expensive and complex, especially when one node is a public blockchain.</p>
</li>
<li><p><strong>Eventual Consistency</strong> — data may temporarily differ, but will eventually converge to a consistent state.</p>
</li>
</ol>
<p>We chose eventual consistency. Why? Once a Solana transaction is confirmed, it's irreversible. There's no rollback. So we need to guarantee that PostgreSQL will eventually receive this data.</p>
<h3 id="heading-the-saga-pattern-long-running-transactions-with-compensation">The Saga Pattern: Long-Running Transactions with Compensation</h3>
<p>The Saga pattern breaks a distributed transaction into a sequence of local transactions. Each step can have a compensating transaction for rollback.</p>
<p>Here's how it could look in our case:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Pseudocode for diploma issuance saga</span>
<span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">SagaStep</span></span> {
    SaveToDatabase,      <span class="hljs-comment">// Step 1</span>
    WriteToBlockchain,   <span class="hljs-comment">// Step 2</span>
    UpdateDatabaseStatus <span class="hljs-comment">// Step 3</span>
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">issue_diploma_saga</span></span>(diploma: Diploma) -&gt; <span class="hljs-built_in">Result</span>&lt;(), SagaError&gt; {
    <span class="hljs-comment">// Step 1: Save to DB with "pending" status</span>
    <span class="hljs-keyword">let</span> db_record = <span class="hljs-keyword">match</span> save_to_database_with_status(&amp;diploma, <span class="hljs-string">"pending"</span>).<span class="hljs-keyword">await</span> {
        <span class="hljs-literal">Ok</span>(record) =&gt; record,
        <span class="hljs-literal">Err</span>(e) =&gt; {
            <span class="hljs-comment">// Nothing to roll back, just exit</span>
            <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(SagaError::DatabaseFailed(e));
        }
    };

    <span class="hljs-comment">// Step 2: Write to blockchain</span>
    <span class="hljs-keyword">let</span> tx_id = <span class="hljs-keyword">match</span> write_to_blockchain(&amp;diploma).<span class="hljs-keyword">await</span> {
        <span class="hljs-literal">Ok</span>(tx) =&gt; tx,
        <span class="hljs-literal">Err</span>(e) =&gt; {
            <span class="hljs-comment">// Compensating transaction: mark record as failed</span>
            mark_database_record_failed(&amp;db_record.id).<span class="hljs-keyword">await</span>?;
            <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(SagaError::BlockchainFailed(e));
        }
    };

    <span class="hljs-comment">// Step 3: Update DB status to "confirmed"</span>
    <span class="hljs-keyword">match</span> update_database_status(&amp;db_record.id, <span class="hljs-string">"confirmed"</span>, &amp;tx_id).<span class="hljs-keyword">await</span> {
        <span class="hljs-literal">Ok</span>(_) =&gt; <span class="hljs-literal">Ok</span>(()),
        <span class="hljs-literal">Err</span>(e) =&gt; {
            <span class="hljs-comment">// Compensation is tricky here: blockchain already has the record</span>
            <span class="hljs-comment">// Can only mark for manual intervention</span>
            mark_for_manual_reconciliation(&amp;db_record.id, &amp;tx_id).<span class="hljs-keyword">await</span>?;
            <span class="hljs-literal">Err</span>(SagaError::InconsistentState(e))
        }
    }
}
</code></pre>
<p><strong>The problem with Saga in blockchain:</strong> Compensating transactions in Solana cost money (gas) and don't actually remove previous entries—they add new ones. This makes the pattern expensive and complex.</p>
<h3 id="heading-idempotency-and-retries">Idempotency and Retries</h3>
<p>Idempotency is the property of an operation yielding the same result on repeated calls. In our context, it's critical.</p>
<p>Here's how we could add a retry mechanism:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> tokio::time::{sleep, Duration};

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_to_database_with_retry</span></span>(
    db_client: &amp;Postgrest,
    data: serde_json::Value,
    max_retries: <span class="hljs-built_in">u32</span>,
) -&gt; <span class="hljs-built_in">Result</span>&lt;(), AppError&gt; {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> retries = <span class="hljs-number">0</span>;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> backoff = Duration::from_millis(<span class="hljs-number">100</span>);

    <span class="hljs-keyword">loop</span> {
        <span class="hljs-keyword">match</span> db_client
            .from(<span class="hljs-string">"credentials"</span>)
            .insert(data.to_string())
            .execute()
            .<span class="hljs-keyword">await</span> 
        {
            <span class="hljs-literal">Ok</span>(response) <span class="hljs-keyword">if</span> response.status().is_success() =&gt; {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(());
            }
            <span class="hljs-literal">Ok</span>(response) <span class="hljs-keyword">if</span> response.status() == <span class="hljs-number">409</span> =&gt; {
                <span class="hljs-comment">// Conflict - record already exists (idempotency!)</span>
                tracing::info!(<span class="hljs-string">"Record already exists, considering it success"</span>);
                <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(());
            }
            <span class="hljs-literal">Ok</span>(_) | <span class="hljs-literal">Err</span>(_) <span class="hljs-keyword">if</span> retries &lt; max_retries =&gt; {
                retries += <span class="hljs-number">1</span>;
                tracing::warn!(
                    <span class="hljs-string">"Database write failed, retry {}/{} after {:?}"</span>,
                    retries, max_retries, backoff
                );
                sleep(backoff).<span class="hljs-keyword">await</span>;
                backoff *= <span class="hljs-number">2</span>; <span class="hljs-comment">// Exponential backoff</span>
            }
            _ =&gt; {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(AppError::Database(
                    <span class="hljs-string">"Failed after maximum retries"</span>.to_string()
                ));
            }
        }
    }
}
</code></pre>
<p><strong>The downside:</strong> If the database is down for an extended period (say, scheduled maintenance), the user waits. Meanwhile, the blockchain transaction is already done!</p>
<h2 id="heading-our-solution-outbox-pattern-with-background-reconciliation">Our Solution: Outbox Pattern with Background Reconciliation</h2>
<p>After analyzing various approaches, we settled on combining two patterns:</p>
<h3 id="heading-the-transactional-outbox-pattern">The Transactional Outbox Pattern</h3>
<p>The essence of the Outbox pattern: instead of writing directly to two systems, we make one atomic transaction to the primary storage, including an event in an outbox table.</p>
<p>Here's how our architecture changes:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// New structure for outbox</span>
<span class="hljs-meta">#[derive(Serialize, Deserialize)]</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">OutboxEvent</span></span> {
    id: Uuid,
    event_type: <span class="hljs-built_in">String</span>,
    payload: serde_json::Value,
    status: <span class="hljs-built_in">String</span>, <span class="hljs-comment">// "pending", "processing", "completed", "failed"</span>
    created_at: DateTime&lt;Utc&gt;,
    processed_at: <span class="hljs-built_in">Option</span>&lt;DateTime&lt;Utc&gt;&gt;,
    retry_count: <span class="hljs-built_in">u32</span>,
    error_message: <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt;,
}

<span class="hljs-comment">// Modified issue_diploma function</span>
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">issue_diploma_with_outbox</span></span>(
    State(state): State&lt;Arc&lt;AppState&gt;&gt;,
    <span class="hljs-keyword">mut</span> multipart: Multipart,
) -&gt; <span class="hljs-built_in">Result</span>&lt;Json&lt;IssueResponse&gt;, AppError&gt; {
    <span class="hljs-comment">// ... parsing and hash generation as before ...</span>

    <span class="hljs-comment">// IMPORTANT: First write to DB in one transaction</span>
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> transaction = state.db_client.begin_transaction().<span class="hljs-keyword">await</span>?;

    <span class="hljs-comment">// Save diploma with "pending_blockchain" status</span>
    <span class="hljs-keyword">let</span> credential_data = serde_json::json!({
        <span class="hljs-string">"hash"</span>: &amp;diploma.hash,
        <span class="hljs-string">"issuer_id"</span>: &amp;diploma.issuer_id,
        <span class="hljs-string">"recipient_id"</span>: &amp;diploma.recipient_id,
        <span class="hljs-string">"status"</span>: <span class="hljs-string">"pending_blockchain"</span>,
        <span class="hljs-string">"issued_at"</span>: diploma.issued_at.to_rfc3339(),
    });

    transaction
        .from(<span class="hljs-string">"credentials"</span>)
        .insert(credential_data.to_string())
        .execute()
        .<span class="hljs-keyword">await</span>?;

    <span class="hljs-comment">// Add event to outbox</span>
    <span class="hljs-keyword">let</span> outbox_event = serde_json::json!({
        <span class="hljs-string">"id"</span>: Uuid::new_v4(),
        <span class="hljs-string">"event_type"</span>: <span class="hljs-string">"WRITE_TO_BLOCKCHAIN"</span>,
        <span class="hljs-string">"payload"</span>: serde_json::to_value(&amp;diploma)?,
        <span class="hljs-string">"status"</span>: <span class="hljs-string">"pending"</span>,
        <span class="hljs-string">"created_at"</span>: Utc::now(),
        <span class="hljs-string">"retry_count"</span>: <span class="hljs-number">0</span>,
    });

    transaction
        .from(<span class="hljs-string">"outbox_events"</span>)
        .insert(outbox_event.to_string())
        .execute()
        .<span class="hljs-keyword">await</span>?;

    <span class="hljs-comment">// Commit transaction - all or nothing!</span>
    transaction.commit().<span class="hljs-keyword">await</span>?;

    <span class="hljs-comment">// Return response to user</span>
    <span class="hljs-literal">Ok</span>(Json(IssueResponse {
        hash: diploma.hash,
        tx_id: <span class="hljs-string">"pending"</span>.to_string(), <span class="hljs-comment">// Will be updated asynchronously</span>
        signature: <span class="hljs-literal">Some</span>(signature),
        issued_at: diploma.issued_at,
    }))
}
</code></pre>
<p>Now we need a background processor for outbox events:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Background worker for outbox processing</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">outbox_processor</span></span>(state: Arc&lt;AppState&gt;) {
    <span class="hljs-keyword">loop</span> {
        <span class="hljs-comment">// Fetch unprocessed events</span>
        <span class="hljs-keyword">let</span> events = fetch_pending_outbox_events(&amp;state.db_client).<span class="hljs-keyword">await</span>;

        <span class="hljs-keyword">for</span> event <span class="hljs-keyword">in</span> events {
            <span class="hljs-keyword">match</span> event.event_type.as_str() {
                <span class="hljs-string">"WRITE_TO_BLOCKCHAIN"</span> =&gt; {
                    process_blockchain_write(event, &amp;state).<span class="hljs-keyword">await</span>;
                }
                _ =&gt; {
                    tracing::warn!(<span class="hljs-string">"Unknown event type: {}"</span>, event.event_type);
                }
            }
        }

        <span class="hljs-comment">// Sleep before next iteration</span>
        tokio::time::sleep(Duration::from_secs(<span class="hljs-number">5</span>)).<span class="hljs-keyword">await</span>;
    }
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">process_blockchain_write</span></span>(
    event: OutboxEvent, 
    state: &amp;Arc&lt;AppState&gt;
) {
    <span class="hljs-keyword">let</span> diploma: Diploma = serde_json::from_value(event.payload.clone())
        .expect(<span class="hljs-string">"Failed to deserialize diploma"</span>);

    <span class="hljs-comment">// Attempt blockchain write</span>
    <span class="hljs-keyword">match</span> state.chain_client.write_hash(&amp;diploma.hash, &amp;diploma).<span class="hljs-keyword">await</span> {
        <span class="hljs-literal">Ok</span>(chain_record) =&gt; {
            <span class="hljs-comment">// Success! Update statuses</span>
            <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> transaction = state.db_client.begin_transaction().<span class="hljs-keyword">await</span>.unwrap();

            <span class="hljs-comment">// Update credential</span>
            transaction
                .from(<span class="hljs-string">"credentials"</span>)
                .update(serde_json::json!({
                    <span class="hljs-string">"status"</span>: <span class="hljs-string">"confirmed"</span>,
                    <span class="hljs-string">"solana_tx_id"</span>: chain_record.tx_id,
                }).to_string())
                .eq(<span class="hljs-string">"hash"</span>, &amp;diploma.hash)
                .execute()
                .<span class="hljs-keyword">await</span>
                .unwrap();

            <span class="hljs-comment">// Mark event as processed</span>
            transaction
                .from(<span class="hljs-string">"outbox_events"</span>)
                .update(serde_json::json!({
                    <span class="hljs-string">"status"</span>: <span class="hljs-string">"completed"</span>,
                    <span class="hljs-string">"processed_at"</span>: Utc::now(),
                }).to_string())
                .eq(<span class="hljs-string">"id"</span>, event.id.to_string())
                .execute()
                .<span class="hljs-keyword">await</span>
                .unwrap();

            transaction.commit().<span class="hljs-keyword">await</span>.unwrap();
        }
        <span class="hljs-literal">Err</span>(e) =&gt; {
            <span class="hljs-comment">// Error - increment retry counter</span>
            update_outbox_event_retry(&amp;state.db_client, event.id, e.to_string()).<span class="hljs-keyword">await</span>;
        }
    }
}
</code></pre>
<h3 id="heading-reconciliation-job-the-safety-net">Reconciliation Job: The Safety Net</h3>
<p>Even with the Outbox pattern, things can go wrong. So we added a background reconciliation process:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Periodic data reconciliation between Solana and PostgreSQL</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">reconciliation_job</span></span>(state: Arc&lt;AppState&gt;) {
    <span class="hljs-keyword">loop</span> {
        tracing::info!(<span class="hljs-string">"Starting reconciliation check..."</span>);

        <span class="hljs-comment">// Fetch recent transactions from Solana (last hour)</span>
        <span class="hljs-keyword">let</span> cutoff_time = Utc::now() - Duration::from_secs(<span class="hljs-number">3600</span>);
        <span class="hljs-keyword">let</span> blockchain_records = fetch_recent_blockchain_transactions(
            &amp;state.chain_client,
            cutoff_time
        ).<span class="hljs-keyword">await</span>;

        <span class="hljs-keyword">for</span> record <span class="hljs-keyword">in</span> blockchain_records {
            <span class="hljs-comment">// Check if record exists in PostgreSQL</span>
            <span class="hljs-keyword">let</span> db_result = state
                .db_client
                .from(<span class="hljs-string">"credentials"</span>)
                .select(<span class="hljs-string">"hash"</span>)
                .eq(<span class="hljs-string">"hash"</span>, &amp;record.hash)
                .single()
                .execute()
                .<span class="hljs-keyword">await</span>;

            <span class="hljs-keyword">if</span> db_result.is_err() || !db_result.unwrap().status().is_success() {
                <span class="hljs-comment">// Record missing in DB - add it</span>
                tracing::warn!(
                    <span class="hljs-string">"Found orphaned blockchain record: hash={}, tx_id={}"</span>, 
                    record.hash, 
                    record.tx_id
                );

                <span class="hljs-comment">// Recover record from blockchain</span>
                <span class="hljs-keyword">let</span> recovery_data = serde_json::json!({
                    <span class="hljs-string">"hash"</span>: record.hash,
                    <span class="hljs-string">"solana_tx_id"</span>: record.tx_id,
                    <span class="hljs-string">"status"</span>: <span class="hljs-string">"recovered_from_blockchain"</span>,
                    <span class="hljs-string">"recovered_at"</span>: Utc::now(),
                    <span class="hljs-comment">// Other fields from transaction metadata</span>
                });

                <span class="hljs-keyword">match</span> state
                    .db_client
                    .from(<span class="hljs-string">"credentials"</span>)
                    .insert(recovery_data.to_string())
                    .execute()
                    .<span class="hljs-keyword">await</span> 
                {
                    <span class="hljs-literal">Ok</span>(_) =&gt; {
                        tracing::info!(<span class="hljs-string">"Successfully recovered record: {}"</span>, record.hash);

                        <span class="hljs-comment">// Alert the team</span>
                        send_alert(
                            <span class="hljs-string">"Data inconsistency detected and fixed"</span>,
                            &amp;<span class="hljs-built_in">format!</span>(<span class="hljs-string">"Recovered hash {} from blockchain"</span>, record.hash)
                        ).<span class="hljs-keyword">await</span>;
                    }
                    <span class="hljs-literal">Err</span>(e) =&gt; {
                        tracing::error!(<span class="hljs-string">"Failed to recover record: {}"</span>, e);
                    }
                }
            }
        }

        <span class="hljs-comment">// Run reconciliation every 5 minutes</span>
        tokio::time::sleep(Duration::from_secs(<span class="hljs-number">300</span>)).<span class="hljs-keyword">await</span>;
    }
}
</code></pre>
<p>Visualizing the new approach:</p>
<pre><code class="lang-plaintext">┌──────────┐     ┌────────────┐      ┌─────────────┐
│  Client  │───▶│ Rust API   │────▶│ PostgreSQL  │
└──────────┘     └────────────┘      │ + Outbox    │
                                     └─────────────┘
                                            │
                                            ▼
                                     ┌─────────────┐
                                     │   SUCCESS   │
                                     │  (Atomic)   │
                                     └─────────────┘
                                            │
                         ┌──────────────────┼──────────────────┐
                         ▼                  ▼                  ▼
                ┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
                │ Outbox Processor│ │Reconciliation│ │  Monitoring  │
                │   (Async)       │ │     Job      │ │   &amp; Alerts   │
                └─────────────────┘ └──────────────┘ └──────────────┘
                         │                  │
                         ▼                  ▼
                   ┌──────────┐      ┌──────────┐
                   │  Solana  │◀────│  Check   │
                   └──────────┘      └──────────┘
</code></pre>
<h2 id="heading-room-for-improvement-looking-ahead">Room for Improvement: Looking Ahead</h2>
<h3 id="heading-queue-with-retries">Queue with Retries</h3>
<p>Instead of a simple outbox in the DB, we could use a proper message queue:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Integration with Redis Streams for more reliable delivery</span>
<span class="hljs-keyword">use</span> redis::AsyncCommands;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">publish_to_queue</span></span>(
    redis_client: &amp;redis::Client,
    diploma: &amp;Diploma,
) -&gt; <span class="hljs-built_in">Result</span>&lt;(), AppError&gt; {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> conn = redis_client.get_async_connection().<span class="hljs-keyword">await</span>?;

    <span class="hljs-keyword">let</span> event = serde_json::json!({
        <span class="hljs-string">"type"</span>: <span class="hljs-string">"WRITE_TO_BLOCKCHAIN"</span>,
        <span class="hljs-string">"payload"</span>: diploma,
        <span class="hljs-string">"timestamp"</span>: Utc::now().to_rfc3339(),
        <span class="hljs-string">"retry_count"</span>: <span class="hljs-number">0</span>,
    });

    <span class="hljs-comment">// Add to Redis Stream with auto-generated ID</span>
    conn.xadd(
        <span class="hljs-string">"diploma:outbox"</span>,
        <span class="hljs-string">"*"</span>,
        &amp;[(<span class="hljs-string">"event"</span>, serde_json::to_string(&amp;event)?)],
    ).<span class="hljs-keyword">await</span>?;

    <span class="hljs-literal">Ok</span>(())
}

<span class="hljs-comment">// Consumer with group for guaranteed delivery</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">consume_from_queue</span></span>(redis_client: &amp;redis::Client, state: Arc&lt;AppState&gt;) {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> conn = redis_client.get_async_connection().<span class="hljs-keyword">await</span>.unwrap();

    <span class="hljs-comment">// Create consumer group</span>
    <span class="hljs-keyword">let</span> _: <span class="hljs-built_in">Result</span>&lt;(), _&gt; = conn.xgroup_create_mkstream(
        <span class="hljs-string">"diploma:outbox"</span>,
        <span class="hljs-string">"blockchain_writers"</span>,
        <span class="hljs-string">"$"</span>,
    ).<span class="hljs-keyword">await</span>;

    <span class="hljs-keyword">loop</span> {
        <span class="hljs-comment">// Read events from queue</span>
        <span class="hljs-keyword">let</span> events: <span class="hljs-built_in">Vec</span>&lt;StreamReadReply&gt; = conn.xreadgroup(
            &amp;[<span class="hljs-string">"diploma:outbox"</span>],
            <span class="hljs-string">"blockchain_writers"</span>,
            <span class="hljs-string">"worker_1"</span>,
            &amp;[<span class="hljs-string">"&gt;"</span>],
            <span class="hljs-literal">Some</span>(<span class="hljs-number">1</span>),
            <span class="hljs-literal">None</span>,
        ).<span class="hljs-keyword">await</span>.unwrap();

        <span class="hljs-keyword">for</span> event <span class="hljs-keyword">in</span> events {
            <span class="hljs-comment">// Process and acknowledge</span>
            process_event(event, &amp;state).<span class="hljs-keyword">await</span>;
            conn.xack(<span class="hljs-string">"diploma:outbox"</span>, <span class="hljs-string">"blockchain_writers"</span>, &amp;[event.id]).<span class="hljs-keyword">await</span>.unwrap();
        }
    }
}
</code></pre>
<h3 id="heading-monitoring-and-alerting">Monitoring and Alerting</h3>
<p>Critical to track system state:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> prometheus::{register_counter_vec, register_histogram_vec, CounterVec, HistogramVec};

lazy_static! {
    <span class="hljs-keyword">static</span> <span class="hljs-keyword">ref</span> INCONSISTENCY_COUNTER: CounterVec = register_counter_vec!(
        <span class="hljs-string">"diploma_inconsistencies_total"</span>,
        <span class="hljs-string">"Total number of data inconsistencies detected"</span>,
        &amp;[<span class="hljs-string">"type"</span>]
    ).unwrap();

    <span class="hljs-keyword">static</span> <span class="hljs-keyword">ref</span> RECONCILIATION_DURATION: HistogramVec = register_histogram_vec!(
        <span class="hljs-string">"reconciliation_duration_seconds"</span>,
        <span class="hljs-string">"Time taken to reconcile records"</span>,
        &amp;[<span class="hljs-string">"status"</span>]
    ).unwrap();
}

<span class="hljs-comment">// Using metrics in code</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">monitor_inconsistency</span></span>(inconsistency_type: &amp;<span class="hljs-built_in">str</span>) {
    INCONSISTENCY_COUNTER
        .with_label_values(&amp;[inconsistency_type])
        .inc();

    <span class="hljs-comment">// Alert if too many inconsistencies</span>
    <span class="hljs-keyword">let</span> total = INCONSISTENCY_COUNTER
        .with_label_values(&amp;[inconsistency_type])
        .get();

    <span class="hljs-keyword">if</span> total &gt; <span class="hljs-number">10.0</span> {
        send_critical_alert(
            <span class="hljs-string">"High inconsistency rate detected"</span>,
            &amp;<span class="hljs-built_in">format!</span>(<span class="hljs-string">"Type: {}, Count: {}"</span>, inconsistency_type, total)
        ).<span class="hljs-keyword">await</span>;
    }
}
</code></pre>
<h3 id="heading-event-sourcing-for-full-traceability">Event Sourcing for Full Traceability</h3>
<p>We could go further and store all events as an immutable log:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[derive(Serialize, Deserialize)]</span>
<span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">DiplomaEvent</span></span> {
    Created {
        hash: <span class="hljs-built_in">String</span>,
        issuer_id: <span class="hljs-built_in">String</span>,
        recipient_id: <span class="hljs-built_in">String</span>,
        timestamp: DateTime&lt;Utc&gt;,
    },
    BlockchainWriteRequested {
        hash: <span class="hljs-built_in">String</span>,
        timestamp: DateTime&lt;Utc&gt;,
    },
    BlockchainWriteCompleted {
        hash: <span class="hljs-built_in">String</span>,
        tx_id: <span class="hljs-built_in">String</span>,
        timestamp: DateTime&lt;Utc&gt;,
    },
    BlockchainWriteFailed {
        hash: <span class="hljs-built_in">String</span>,
        error: <span class="hljs-built_in">String</span>,
        retry_count: <span class="hljs-built_in">u32</span>,
        timestamp: DateTime&lt;Utc&gt;,
    },
    ReconciliationDetected {
        hash: <span class="hljs-built_in">String</span>,
        source: <span class="hljs-built_in">String</span>, <span class="hljs-comment">// "blockchain" or "database"</span>
        timestamp: DateTime&lt;Utc&gt;,
    },
}

<span class="hljs-comment">// This gives us complete history for every diploma</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">append_event</span></span>(
    db_client: &amp;Postgrest,
    event: DiplomaEvent,
) -&gt; <span class="hljs-built_in">Result</span>&lt;(), AppError&gt; {
    <span class="hljs-keyword">let</span> event_data = serde_json::json!({
        <span class="hljs-string">"event_type"</span>: event.variant_name(),
        <span class="hljs-string">"payload"</span>: serde_json::to_value(&amp;event)?,
        <span class="hljs-string">"timestamp"</span>: Utc::now(),
    });

    db_client
        .from(<span class="hljs-string">"diploma_events"</span>)
        .insert(event_data.to_string())
        .execute()
        .<span class="hljs-keyword">await</span>?;

    <span class="hljs-literal">Ok</span>(())
}
</code></pre>
<h2 id="heading-conclusion-lessons-from-the-trenches">Conclusion: Lessons from the Trenches</h2>
<p>Working with dual-writes between Solana and PostgreSQL taught us several hard lessons:</p>
<ol>
<li><p><strong>Never trust sequential calls</strong> — just because the first one succeeds doesn't guarantee the second will. Especially when the first is an irreversible blockchain operation.</p>
</li>
<li><p><strong>Design for failure</strong> — it's not a question of if the system will fail, but when. The Outbox pattern and background reconciliation aren't redundancy; they're necessities.</p>
</li>
<li><p><strong>Eventual consistency is your friend</strong> — don't try to achieve strong consistency between blockchain and traditional databases. It's expensive, complex, and often impossible.</p>
</li>
<li><p><strong>Monitoring is critical</strong> — better to get an alert about drift within a minute than hear about it from a user a week later.</p>
</li>
<li><p><strong>Idempotency saves lives</strong> — design operations so they can be safely retried. This simplifies recovery from failures.</p>
</li>
</ol>
<p>For fellow engineers working with Web3 backends in Rust: blockchain isn't a silver bullet. It's a powerful tool, but it requires careful system design. Dual-writes seem simple until you hit your first production failure at 3 AM.</p>
<p>Remember: in distributed systems, everything that can go wrong will go wrong. Design accordingly.</p>
<h2 id="heading-useful-links">Useful Links</h2>
<ul>
<li><p><a target="_blank" href="https://microservices.io/patterns/data/saga.html">Saga Pattern - Microservices.io</a></p>
</li>
<li><p><a target="_blank" href="https://microservices.io/patterns/data/transactional-outbox.html">Transactional Outbox Pattern</a></p>
</li>
<li><p><a target="_blank" href="https://doc.rust-lang.org/book/">Event Sourcing in Rust</a></p>
</li>
<li><p><a target="_blank" href="https://docs.solana.com/">Building on Solana with Rust</a></p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/">PostgreSQL and Distributed Systems</a></p>
</li>
<li><p><a target="_blank" href="https://dataintensive.net/">Designing Data-Intensive Applications</a> — the bible for distributed systems</p>
</li>
</ul>
<p>If you have experience solving similar problems or questions about implementation, let's discuss in the comments. I'm particularly interested in hearing about alternative approaches to syncing blockchain with traditional databases.</p>
]]></content:encoded></item></channel></rss>