{"id":50189600,"url":"https://github.com/puneethkumarck/prism","last_synced_at":"2026-05-25T12:04:13.347Z","repository":{"id":350444327,"uuid":"1206436763","full_name":"Puneethkumarck/prism","owner":"Puneethkumarck","description":"Prism — High-performance real-time Solana transaction indexer built with Java 25, Helidon 4 SE, Virtual Threads, pgjdbc COPY protocol, and hexagonal architecture. Streams via Yellowstone gRPC or free WebSocket. No Spring Boot.","archived":false,"fork":false,"pushed_at":"2026-05-08T12:37:27.000Z","size":504,"stargazers_count":1,"open_issues_count":67,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-08T14:34:23.464Z","etag":null,"topics":["blockchain","grpc","helidon","hexagonal-architecture","indexer","java","postgresql","real-time","solana","virtual-threads"],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Puneethkumarck.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-09T23:12:34.000Z","updated_at":"2026-04-30T01:26:49.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Puneethkumarck/prism","commit_stats":null,"previous_names":["puneethkumarck/prism"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Puneethkumarck/prism","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fprism","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fprism/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fprism/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fprism/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Puneethkumarck","download_url":"https://codeload.github.com/Puneethkumarck/prism/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fprism/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33473721,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-25T06:32:55.349Z","status":"ssl_error","status_checked_at":"2026-05-25T06:32:35.322Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["blockchain","grpc","helidon","hexagonal-architecture","indexer","java","postgresql","real-time","solana","virtual-threads"],"created_at":"2026-05-25T12:04:12.188Z","updated_at":"2026-05-25T12:04:13.341Z","avatar_url":"https://github.com/Puneethkumarck.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n![Build](https://github.com/Puneethkumarck/prism/actions/workflows/ci.yml/badge.svg)\n![Java 25](https://img.shields.io/badge/Java-25_LTS-ED8B00?style=for-the-badge\u0026logo=openjdk\u0026logoColor=white)\n![Helidon](https://img.shields.io/badge/Helidon-4.4_SE-00569C?style=for-the-badge\u0026logo=oracle\u0026logoColor=white)\n![Virtual Threads](https://img.shields.io/badge/Virtual_Threads-Project_Loom-7B1FA2?style=for-the-badge\u0026logo=java\u0026logoColor=white)\n![PostgreSQL](https://img.shields.io/badge/PostgreSQL-16-4169E1?style=for-the-badge\u0026logo=postgresql\u0026logoColor=white)\n![Solana](https://img.shields.io/badge/Solana-Mainnet-9945FF?style=for-the-badge\u0026logo=solana\u0026logoColor=white)\n![Architecture](https://img.shields.io/badge/Architecture-Hexagonal-purple?style=for-the-badge)\n![No Spring Boot](https://img.shields.io/badge/No-Spring_Boot-red?style=for-the-badge)\n\n# 🔺 Prism\n\n### Refract the Solana firehose into a queryable data stream.\n\n**A zero-Spring, zero-JPA, real-time Solana transaction indexer built on Java 25 Virtual Threads and Helidon 4 SE.**\nStreams confirmed transactions via Yellowstone gRPC (paid) or WebSocket `blockSubscribe` (free), persists them with the PostgreSQL `COPY` protocol, and serves them over a paginated REST API — all with sub-100ms startup and a \u003c50 MB resident footprint.\n\n[Why Prism?](#-why-does-this-exist) · [Architecture](#-architecture) · [The Hot Path](#-the-hot-path-how-a-transaction-becomes-a-row) · [Quick Start](#-quick-start) · [API Reference](#-api-reference) · [Tech Stack](#-tech-stack)\n\n\u003c/div\u003e\n\n---\n\n## 🎬 The Problem\n\nSolana produces a new block roughly every **400 milliseconds**. At any given moment, mainnet can push **50,000+ transactions per second** through the firehose. If you want to know what happened on chain — payments, memos, failed swaps, large transfers — you have exactly three options:\n\n| Option | What Happens | 💀 Verdict |\n|---|---|---|\n| 🐢 **Poll `getBlock`** | `getBlock(slot)` → repeat, repeat, repeat | Falls behind in minutes. Burns RPC credits. Dies at 50K TPS. |\n| 🏗️ **Use a hosted indexer** | Pay per query. Sit behind someone else's cache. Hope their schema fits | $$$, schema lock-in, no custom parsing |\n| 🚀 **Stream + batch yourself** | Subscribe to Geyser/WebSocket, parse in-process, batch write to Postgres | Full control. Sub-second freshness. Your schema, your queries. |\n\nPrism is option 3 — a sharp, opinionated take on **option 3** — written in Java 25 with Virtual Threads, Helidon 4 SE, and raw pgjdbc. No Spring Boot. No JPA. No reflection. Just a tight hot path from socket to row.\n\n## 💡 The Solution\n\n```text\n 🌊 Solana                 🔺 Prism                     🗄️  PostgreSQL\n ─────────                 ──────                       ─────────────\n                                                        transactions\n Yellowstone gRPC   ──►   stream adapter    ──► COPY ──► failed_tx\n (or WebSocket)           │                            ─► memos\n                          ▼                            ─► large_transfers\n                   LinkedTransferQueue                 ─► accounts\n                   (unbounded, lock-free)                     ▲\n                          │                                   │\n                          ▼                            ──────┘\n                  TransactionBatchService              parallel writes\n                  (200 tx / 100ms dual-trigger)        on virtual threads\n                          │\n                          ▼\n                  TransactionProcessor\n                  split → [success | failed | memo | transfer]\n```\n\n## 🎯 The Result\n\nA real-time pipeline that stays caught up with mainnet, survives RPC flaps, flushes batches in ~100ms, and exposes 8 paginated read endpoints — all inside a JVM that starts in under a second and sips heap.\n\n\u003cdiv align=\"center\"\u003e\n\n| Metric | Value |\n|--------|-------|\n| **Throughput target** | ~99.5% indexing efficiency at mainnet velocity |\n| **Write strategy** | PostgreSQL `COPY FROM STDIN` + staging merge — **5-10× faster** than `INSERT VALUES` |\n| **Startup time** | \u003c 100 ms (Helidon 4 SE, no classpath scanning) |\n| **Thread model** | Virtual Threads — no reactive `.flatMap().subscribeOn()` gymnastics |\n| **Backpressure** | Unbounded tx queue, bounded account queue — zero disconnects from Yellowstone |\n| **Streaming modes** | 🆓 WebSocket `blockSubscribe` (free) · 💰 Yellowstone gRPC (paid) |\n| **Stack size** | Helidon 4 SE + pgjdbc + Avaje Inject + Micrometer — **NO Spring Boot** |\n\n\u003c/div\u003e\n\n---\n\n## 📚 Table of Contents\n\n- [🤔 Why Does This Exist?](#-why-does-this-exist)\n- [🌈 Why \"Prism\"?](#-why-prism)\n- [🎞️ A Day in the Life of a Solana Transaction](#%EF%B8%8F-a-day-in-the-life-of-a-solana-transaction)\n- [⚡ The Hot Path: How a Transaction Becomes a Row](#-the-hot-path-how-a-transaction-becomes-a-row)\n- [🏛️ Architecture](#%EF%B8%8F-architecture)\n- [🧬 The COPY Protocol: Why We Bypass `INSERT VALUES`](#-the-copy-protocol-why-we-bypass-insert-values)\n- [🧵 Virtual Threads: Why No Reactor, No WebFlux, No Spring Boot](#-virtual-threads-why-no-reactor-no-webflux-no-spring-boot)\n- [🌊 Dual Streaming Modes: Free or Fast](#-dual-streaming-modes-free-or-fast)\n- [🪣 Dual-Trigger Batching: Size OR Time](#-dual-trigger-batching-size-or-time)\n- [🔁 The Reconnect Dance](#-the-reconnect-dance)\n- [🛠️ Tech Stack](#%EF%B8%8F-tech-stack)\n- [🧱 Module Structure](#-module-structure)\n- [🚀 Quick Start](#-quick-start)\n- [🎛️ Make Targets](#%EF%B8%8F-make-targets)\n- [🌐 API Reference](#-api-reference)\n- [⚙️ Configuration Reference](#%EF%B8%8F-configuration-reference)\n- [📊 Observability](#-observability)\n- [🧪 Testing Strategy](#-testing-strategy)\n- [🗂️ Database Schema](#%EF%B8%8F-database-schema)\n- [🧠 Design Decisions, Quick Reference](#-design-decisions-quick-reference)\n- [📜 License](#-license)\n\n---\n\n## 🤔 Why Does This Exist?\n\nBecause every on-chain product eventually asks the same questions:\n\n- 💸 *\"Did our transaction confirm?\"*\n- 💰 *\"Which wallets just moved more than 1 SOL?\"*\n- 📝 *\"Did the customer include a memo with that payment?\"*\n- 🚫 *\"How many of our swaps failed in the last hour?\"*\n- 🏦 *\"What's the current balance of every fee payer we've seen?\"*\n\nThese questions need **fresh, queryable, relational** data — not JSON-RPC round-trips to a Solana RPC node, and not someone else's hosted indexer. You need *your* Postgres with *your* indexes and *your* schema, populated a few hundred milliseconds after finality.\n\nPrism answers all five questions out of the box.\n\n\u003e **🎯 Design principle:** Hot path stays **synchronous and boring**. No reactive, no actors, no fancy schedulers. One virtual thread per job. One queue per concurrency boundary. The JVM does the rest.\n\n---\n\n## 🌈 Why \"Prism\"?\n\nBecause a prism takes one stream of white light and **splits it into the colors that were always there** — it doesn't create information, it reveals structure.\n\nThat's exactly what the indexer does to Solana's stream:\n\n```text\n                    ┌──────────────────────────────────────┐\n                    │                                      │\n ✨ Solana stream ──┤  🔺 Prism                            │\n  (undifferentiated)│                                      │\n                    │   ┌─► 🟢 successful transactions    │\n                    │   ├─► 🔴 failed transactions         │\n                    │   ├─► 🟡 large transfers (\u003e1 SOL)    │\n                    │   ├─► 🟣 memos                       │\n                    │   └─► 🔵 fee payer accounts          │\n                    │                                      │\n                    └──────────────────────────────────────┘\n                        five queryable tables, refracted\n                        out of one protobuf soup\n```\n\nEvery incoming transaction is refracted into the projections you actually want to query. No more scanning JSON blobs. No more `getBlock` loops. Just `SELECT`.\n\n---\n\n## 🎞️ A Day in the Life of a Solana Transaction\n\n\u003e **🎬 Scene 1 — Somewhere in a Solana validator, 400 ms ago**\n\n```text\n ⚙️  validator          📡  Geyser plugin           🔺  Prism\n ─────────              ──────────────              ───────\n\n 🧱 builds slot 312_701                                 │\n 📝 includes tx 5Kx7aLm...                              │\n ✉️  finalizes block                                    │\n                        🚀 \"new tx!\"  ─────────►   📥 arrives on gRPC\n                                                        │\n                                                   🔍 TransactionParser\n                                                        │\n                                                        │   signature: 5Kx7a...\n                                                        │   slot:      312_701\n                                                        │   amount:    4.2 SOL\n                                                        │   from:      7xKX...h9Fz\n                                                        │   to:        9vBM...n3Tr\n                                                        │   memo:      \"invoice #7341\"\n                                                        │   failed:    false\n                                                        ▼\n                                                   LinkedTransferQueue (unbounded)\n                                                        │\n                                                        ▼\n                                                   📦 TransactionBatchService\n                                                        │   (accumulating...)\n                                                        │   199 tx + this one = 200 → 🚽 FLUSH\n                                                        ▼\n                                                   🔀 TransactionProcessor\n                                                        │\n                                ┌───────────────────────┼────────────────────────┐\n                                ▼                       ▼                        ▼\n                         📄 transactions        📝 memos                  💰 large_transfers\n                         (COPY FROM STDIN)      (batch INSERT)            (batch INSERT)\n                         staging → merge        reWriteBatched=true       reWriteBatched=true\n                                                                                 │\n                                                                                 ▼\n                                                                           done in ~8 ms\n                                                                           total, parallel\n```\n\n**Timeline for one transaction:**\n\n| Stage | Latency | What happens |\n|---|---|---|\n| 📡 Geyser publish | ~5 ms | Validator plugin flushes to wire |\n| 🚚 Network hop | ~10-30 ms | HTTP/2 frame to Prism |\n| 🔍 Parse | \u003c 1 ms | Protobuf → domain record |\n| 🪣 Queue | \u003c 1 μs | `LinkedTransferQueue.offer()` |\n| 📦 Batch wait | 0-100 ms | Dual-trigger: 200 tx OR 100 ms |\n| 🖊️ COPY + merge | ~5-10 ms | 200 rows in one write |\n| ✅ **End-to-end** | **\u003c 200 ms** | From finality to queryable row |\n\n\u003e **Scene 2 — A developer runs `curl localhost:3000/api/transfers?min_amount=4.0` and sees the transaction from Scene 1 in the results.** That's the whole movie.\n\n---\n\n## ⚡ The Hot Path: How a Transaction Becomes a Row\n\nThe write side is the most interesting part of the system. It's designed to be boring, fast, and **impossible to back-pressure**.\n\n```mermaid\nflowchart LR\n    subgraph Source[\"🌊 Solana Source\"]\n        direction TB\n        YS[\"Yellowstone gRPC\u003cbr/\u003e(paid)\"]\n        WS[\"WebSocket\u003cbr/\u003eblockSubscribe\u003cbr/\u003e(free)\"]\n    end\n\n    subgraph Parse[\"🔍 Parsing\"]\n        P1[\"TransactionParser\u003cbr/\u003e\u003ci\u003eprotobuf / json\u003c/i\u003e\"]\n        P2[\"BlockNotificationParser\u003cbr/\u003e\u003ci\u003eshared logic\u003c/i\u003e\"]\n    end\n\n    subgraph Queue[\"🪣 Concurrency Boundary\"]\n        direction TB\n        TQ[\"LinkedTransferQueue\u003cbr/\u003e\u003cb\u003eunbounded\u003c/b\u003e\u003cbr/\u003etx stream\"]\n        AQ[\"ArrayBlockingQueue(10K)\u003cbr/\u003e\u003cb\u003ebounded, drop-if-full\u003c/b\u003e\u003cbr/\u003eaccount stream\"]\n    end\n\n    subgraph Batch[\"📦 Batching\"]\n        direction TB\n        TB[\"TransactionBatchService\u003cbr/\u003e200 tx / 100 ms\"]\n        AB[\"AccountBatchService\u003cbr/\u003e200 acct / 2 s\u003cbr/\u003e+ dedup by pubkey\"]\n    end\n\n    subgraph Processor[\"🔀 Processor\"]\n        TP[\"TransactionProcessor\u003cbr/\u003e\u003ci\u003esplit into 4 buckets\u003c/i\u003e\"]\n    end\n\n    subgraph DB[\"🗄️ PostgreSQL\"]\n        direction TB\n        T1[\"transactions\u003cbr/\u003e\u003cb\u003eCOPY FROM STDIN\u003c/b\u003e\"]\n        T2[\"failed_transactions\u003cbr/\u003ebatch INSERT\"]\n        T3[\"memos\u003cbr/\u003ebatch INSERT\"]\n        T4[\"large_transfers\u003cbr/\u003ebatch INSERT\"]\n        T5[\"accounts\u003cbr/\u003eUPSERT ON CONFLICT\"]\n    end\n\n    YS --\u003e P1 --\u003e TQ\n    WS --\u003e P2 --\u003e TQ\n    P1 --\u003e AQ\n    P2 --\u003e AQ\n    TQ --\u003e TB --\u003e TP\n    AQ --\u003e AB --\u003e T5\n    TP --\u003e T1\n    TP --\u003e T2\n    TP --\u003e T3\n    TP --\u003e T4\n\n    style T1 fill:#4caf50,color:#fff\n    style YS fill:#9945FF,color:#fff\n    style WS fill:#00D18C,color:#fff\n```\n\n**Two concurrency boundaries, two queue strategies:**\n\n| Queue | Type | Capacity | Policy | Why |\n|---|---|---|---|---|\n| 🪣 **Transaction** | `LinkedTransferQueue` | **Unbounded** | Never blocks producer | If this queue blocks, Yellowstone hangs up with a `lagged` error. Losing a transaction is worse than using heap. |\n| 🪣 **Account** | `ArrayBlockingQueue` | **10,000** | `try_offer` — drop if full | Accounts are less critical and dedup-friendly. Dropping the occasional fee payer snapshot is fine. |\n\nThis asymmetry is the whole trick. Transactions get backpressure protection; accounts get memory protection. Nobody wins both fights at once.\n\n---\n\n## 🏛️ Architecture\n\nPrism follows strict **hexagonal architecture (ports \u0026 adapters)** with DDD tactical patterns. Dependencies always point inward.\n\n```text\n                        ┌───────────────────────────────────────────┐\n                        │                                           │\n                        │         🏛️  application/                  │\n                        │    ┌────────────────────────────────┐     │\n                        │    │ Helidon 4 SE functional routes │     │\n                        │    │ IndexerApplication (main)      │     │\n                        │    │ IndexerConfig (env parsing)    │     │\n                        │    │ GlobalErrorHandler             │     │\n                        │    │ MapStruct mappers              │     │\n                        │    └────────────────────────────────┘     │\n                        │                  │                        │\n                        │                  ▼ delegates to            │\n                        │    ┌────────────────────────────────┐     │\n                        │    │         🧠 domain/             │     │\n                        │    │                                │     │\n                        │    │  ┌──── model ────┐             │     │\n                        │    │  │ Signature     │             │     │\n                        │    │  │ Pubkey        │             │     │\n                        │    │  │ Slot          │             │     │\n                        │    │  │ SolanaTx      │             │     │\n                        │    │  │ Account       │             │     │\n                        │    │  └───────────────┘             │     │\n                        │    │                                │     │\n                        │    │  ┌──── service ──┐             │     │\n                        │    │  │ BatchService  │             │     │\n                        │    │  │ Processor     │             │     │\n                        │    │  │ LargeTransfer │             │     │\n                        │    │  │   Filter      │             │     │\n                        │    │  └───────────────┘             │     │\n                        │    │                                │     │\n                        │    │  ┌──── port ─────┐             │     │\n                        │    │  │ TxStream      │◄── implemented by\n                        │    │  │ TxRepo        │             │     │\n                        │    │  │ MemoRepo      │             │     │\n                        │    │  │ ...           │             │     │\n                        │    │  └───────────────┘             │     │\n                        │    │                                │     │\n                        │    │  ZERO framework imports.       │     │\n                        │    │  Only Lombok + java.*          │     │\n                        │    └────────────────────────────────┘     │\n                        │                  ▲                        │\n                        │                  │ implements ports       │\n                        │    ┌────────────────────────────────┐     │\n                        │    │    🔌 infrastructure/          │     │\n                        │    │                                │     │\n                        │    │  grpc/        Yellowstone       │     │\n                        │    │  websocket/   blockSubscribe    │     │\n                        │    │  persistence/ pgjdbc + COPY     │     │\n                        │    │  metrics/     Micrometer        │     │\n                        │    │  console/     ANSI formatter    │     │\n                        │    │  solana/      Base58, balance   │     │\n                        │    └────────────────────────────────┘     │\n                        │                                           │\n                        └───────────────────────────────────────────┘\n\n                        🛡️ ArchUnit enforces these rules at build time\n```\n\n**The rules (enforced by ArchUnit):**\n\n| Rule | What It Stops |\n|---|---|\n| `domain` ⊥ `infrastructure` | Prevents domain from leaking JDBC/gRPC types |\n| `domain` ⊥ `application` | Prevents domain from reaching up into routes |\n| `domain` has **zero** Helidon/Jakarta imports | Keeps domain framework-free (Lombok + `java.*` only) |\n| `domain` has **zero** `java.sql.*` imports | No DB types in business logic |\n| `infrastructure` ⊥ `application.routing` | Infra adapters can't call routes directly |\n\nBreak any rule and the build fails. No social contracts, only compile errors.\n\n---\n\n## 🧬 The COPY Protocol: Why We Bypass `INSERT VALUES`\n\nPostgreSQL has two fundamentally different write paths. Most ORMs use the slower one. Prism uses the faster one.\n\n\u003e **🎬 The 5× speedup you get by ignoring your instincts**\n\n```text\n ❌ INSERT VALUES (what JPA / Hibernate / Spring Data give you)\n ─────────────────────────────────────────────────────────────\n INSERT INTO transactions VALUES ($1, $2, $3);\n INSERT INTO transactions VALUES ($4, $5, $6);\n INSERT INTO transactions VALUES ($7, $8, $9);\n ... × 200\n\n Each row:\n   🔄 parse SQL\n   📋 plan query\n   🔒 acquire lock\n   💾 write WAL\n   📝 update index\n   ✅ commit row\n\n 200 rows × overhead = 💀 slow\n\n\n ✅ COPY FROM STDIN (what pgjdbc's CopyManager gives you)\n ──────────────────────────────────────────────────────\n COPY staging_transactions (signature, slot, success) FROM STDIN (FORMAT TEXT);\n 5Kx7a...\t312701\tt\n 6Lm8b...\t312701\tt\n 7Nv9c...\t312701\tt\n ... × 200\n \\.\n \n INSERT INTO transactions SELECT * FROM staging_transactions\n   ON CONFLICT (signature) DO NOTHING;\n TRUNCATE staging_transactions;\n \n One batch:\n   🔄 parse SQL once\n   📋 plan query once\n   🚀 stream 200 rows over STDIN\n   💾 one WAL flush\n   📝 index update once\n   ✅ commit batch\n\n 5-10× faster on the hottest table 🔥\n```\n\n**Why a staging table?** `COPY` doesn't support `ON CONFLICT`. So we:\n\n1. `COPY` into `staging_transactions` (no constraints, no indexes, pure speed)\n2. `INSERT ... SELECT ... ON CONFLICT (signature) DO NOTHING` from staging → main\n3. `TRUNCATE staging_transactions` and repeat\n\nThe staging merge costs an extra statement, but `COPY` + merge is still ~5× faster than individual `INSERT`s because the expensive parts — parsing, planning, locking, WAL — amortize across 200 rows.\n\n\u003e **💡 Secondary tables** (`failed_transactions`, `memos`, `large_transfers`) are low volume, so they use plain `PreparedStatement.addBatch()` with `reWriteBatchedInserts=true` on the pgjdbc URL. The driver rewrites `INSERT ... VALUES ($1, $2)` batches into a single `INSERT ... VALUES ($1, $2), ($3, $4), ...` statement — nearly `COPY`-level throughput without the staging dance.\n\n---\n\n## 🧵 Virtual Threads: Why No Reactor, No WebFlux, No Spring Boot\n\nTraditional Java servers tried to solve the C10K problem with reactive streams:\n\n```java\n// Reactive way — every I/O op is a callback in a chain\nreturn webClient.get()\n    .uri(\"/slot\")\n    .retrieve()\n    .bodyToMono(Slot.class)\n    .flatMap(slot -\u003e repo.findBySlot(slot))\n    .flatMap(txs -\u003e Flux.fromIterable(txs)\n        .parallel()\n        .runOn(Schedulers.boundedElastic())\n        .map(this::process)\n        .sequential()\n        .collectList())\n    .onErrorResume(e -\u003e Mono.error(new IndexerException(e)));\n```\n\nThat's fine code. It's also impossible to debug, step through, or reason about at 3 AM during an incident.\n\n**Virtual Threads (Project Loom, finalized in JDK 21) change the rules.** You can write plain blocking code and the JVM parks the virtual thread on any I/O wait — no OS thread is held, no carrier is pinned, no Schedulers, no operators:\n\n```java\n// Loom way — boring, blocking, testable\nvar slot = httpClient.get(\"/slot\", Slot.class);\nvar txs = repo.findBySlot(slot);\nfor (var tx : txs) {\n    process(tx);\n}\n```\n\nOne virtual thread per job. The JVM multiplexes millions onto a handful of carrier threads. Helidon 4 SE was built from the ground up on this model — it has no Netty event loop, no Servlet container, no CDI graph to warm up. Startup is under 100 ms and p99.999 latency is under 7 ms.\n\n**Prism's one rule:** never call `synchronized`, always use `ReentrantLock`. `synchronized` pins a virtual thread to its carrier and kills throughput. ArchUnit enforces this at build time.\n\n---\n\n## 🌊 Dual Streaming Modes: Free or Fast\n\nPrism can consume Solana's transaction stream in two ways — **same domain, same batching, same persistence** — through a pluggable `TransactionStream` port.\n\n```text\n                           ┌─────────────────────────┐\n                           │  TransactionStream port │\n                           │  (domain interface)     │\n                           └────────────┬────────────┘\n                                        │\n                  ┌─────────────────────┴─────────────────────┐\n                  │                                           │\n         ┌────────┴─────────┐                        ┌────────┴────────┐\n         │ 🆓 WebSocket      │                        │ 💰 Yellowstone   │\n         │ blockSubscribe    │                        │ gRPC (Geyser)    │\n         └──────────────────┘                         └─────────────────┘\n```\n\n| | 🆓 WebSocket mode | 💰 gRPC mode |\n|---|---|---|\n| **Endpoint** | `wss://api.mainnet-beta.solana.com` | Paid Yellowstone provider |\n| **Protocol** | JSON-RPC `blockSubscribe` over WS | Protobuf over HTTP/2 |\n| **Cost** | **$0** — public RPC | **$300-500/mo** typical |\n| **Latency** | Higher — JSON parse + `confirmed` commitment | Lower — native protobuf + direct Geyser |\n| **Throughput** | Lower — JSON overhead | Higher — 8 MB HTTP/2 window |\n| **Stability** | Public RPC can be flaky | Dedicated, SLA-backed |\n| **Vote filtering** | Client-side (check Vote program) | Server-side (`vote: false` filter) |\n| **Tx data** | `encoding: \"jsonParsed\"`, full | Raw protobuf (richer) |\n| **When to use** | Dev, testnet, hobby projects, low TPS | Production, mainnet, real workloads |\n\nSwitch with a single env var — `STREAM_MODE=websocket` (default) or `STREAM_MODE=grpc`. The domain layer doesn't know or care.\n\n\u003e **⚠️ Known HTTP/2 limitation (Helidon 4.4):** The stream-level window is a configurable 8 MiB, but Helidon doesn't yet expose the connection-level window (defaults to 64 KiB per RFC 7540). In practice, Helidon emits `WINDOW_UPDATE` frames as data is consumed, so throughput is gated by consumption speed — not a static cap. Tracked in the `docs/implementation-plan.md` for a future revisit.\n\n---\n\n## 🪣 Dual-Trigger Batching: Size OR Time\n\nThe worst thing you can do to a write-heavy Postgres workload is flush one row at a time. The second-worst thing is to wait forever for a batch that never fills up.\n\nPrism uses **dual-trigger batching** — flush when *either* threshold fires.\n\n```text\n                    ┌──────────────────────────────────────┐\n                    │ 📦 TransactionBatchService           │\n                    │                                      │\n                    │  Buffer: [ 🟦 🟦 🟦 🟦 🟦 ... ]     │\n                    │                                      │\n                    │  Trigger A: size ≥ 200 txs           │\n                    │  Trigger B: elapsed ≥ 100 ms         │\n                    │                                      │\n                    │  Whichever fires first → FLUSH 🚽    │\n                    └──────────────────────────────────────┘\n\n ⚡ High TPS (40K/s)       🪶 Low TPS (100/s)        💤 Idle (0/s)\n ──────────────────        ──────────────────         ────────────\n 200 txs in 5 ms           200 txs in 2 sec           0 txs\n → size triggers           → time triggers            → no flush\n → flush every 5 ms        → flush every 100 ms       → buffer stays empty\n ~200× fewer round-trips   bounded max latency         no wasted writes\n```\n\n**The numbers:**\n\n| Scenario | TPS in | Batches/s | DB round-trips/s | Max write latency |\n|---|---|---|---|---|\n| Mainnet burst | 40,000 | ~200 | ~200 | 5 ms |\n| Typical mainnet | 4,000 | ~40 | ~40 | 50 ms |\n| Quiet dev chain | 100 | 10 | 10 | 100 ms |\n\nCompare that to naive per-row writes at 40K TPS: **40,000 round-trips per second**. The database would melt.\n\n**Account batching uses the same pattern with different thresholds** (200 / 2,000 ms) because account upserts are less latency-sensitive and dedup well — the same `pubkey` often appears multiple times in a 2-second window, and we keep the one with the highest slot in memory before sending a single `UPSERT`.\n\n---\n\n## 🔁 The Reconnect Dance\n\nSolana RPC endpoints — whether free public or paid Yellowstone — will drop you. Count on it. Here's what happens when they do:\n\n```text\n T+0s     🌊 stream is flowing... transactions pouring in\n T+120s   💥 stream ends unexpectedly (TCP reset / GOAWAY / network blip)\n          │\n          │    🧮 attempt 1: delay = 2 × 2¹ = 4 s\n          ▼\n T+124s   🔄 retry → connected! streaming resumes\n T+184s   ✅ 60 s of stable flow → attempt counter resets to 0\n          │\n          │    🎉 next disconnect will start at 4 s again\n          │\n          ▼\n ...\n T+900s   💥 another disconnect\n          │    attempt 1: 4 s  → fails\n          │    attempt 2: 8 s  → fails\n          │    attempt 3: 16 s → fails\n          │    attempt 4: 30 s (capped) → connects\n          ▼\n T+958s   ✅ back online, counter resets after 60 s stable\n```\n\n**Formula:** `delay = base × 2^min(attempt, 4)` where `base = 2 s`, capped at `30 s`.\n\n| Attempt | Computed | Actual Delay |\n|---|---|---|\n| 1 | 4 s | **4 s** |\n| 2 | 8 s | **8 s** |\n| 3 | 16 s | **16 s** |\n| 4 | 32 s | **30 s** (capped) |\n| 5+ | 32 s | **30 s** (capped) |\n\n**Reset rule:** after **60 seconds** of stable connection, attempt counter resets to 0 — so transient blips don't accumulate into slow restarts.\n\nThe same `ReconnectHandler` is shared by both the gRPC and WebSocket adapters. One strategy, two transports.\n\n---\n\n## 🛠️ Tech Stack\n\n| Component | Choice | Version | Why |\n|-----------|--------|---------|-----|\n| **Runtime** | Java + Virtual Threads | 25 LTS | Scoped Values finalized, +291% VT throughput vs JDK 21 |\n| **HTTP server** | Helidon 4 SE | 4.4.0 | Built on VTs from the ground up, \u003c7 ms p99.999, \u003c50 MB RSS, \u003c100 ms startup, no CDI/reflection |\n| **gRPC client** | Helidon 4 SE gRPC | 4.4.0 | Built-in HTTP/2 engine, VT-native, no grpc-java |\n| **DI** | Avaje Inject | latest | Compile-time codegen, JSR-330 (`@Singleton`), zero reflection |\n| **DB driver** | pgjdbc | 42.7+ | `CopyManager` + `reWriteBatchedInserts=true` |\n| **Connection pool** | HikariCP × 2 | 7.x | Dual pools: write (20) + read (20) |\n| **JSON** | Jackson | 2.18+ | Helidon native media support |\n| **Migrations** | Flyway (standalone) | 12.x | No Spring integration, runs in `main()` |\n| **Resilience** | Resilience4j | 2.3+ | Reconnect backoff strategy |\n| **Metrics** | Micrometer + Prometheus | 1.14+ | Native Helidon integration |\n| **Mapping** | MapStruct | 1.6.3 | Compile-time, `componentModel = \"jsr330\"` |\n| **Architecture tests** | ArchUnit | 1.4.1 | Hexagonal rules enforced at build time |\n| **Logging** | SLF4J + Logback | — | Structured via `@Slf4j` |\n| **Testing** | JUnit 5 + Mockito BDD + AssertJ + Testcontainers + Awaitility | — | Three source sets: unit, integration, fixtures |\n| **Build** | Gradle (Kotlin DSL) + convention plugins | 9.0 | `prism.service` + `prism.library` in `buildSrc/` |\n\n### ❌ What Prism Explicitly Does Not Use\n\n| Avoided | Replacement | Why |\n|---|---|---|\n| **Spring Boot** | Helidon 4 SE + `public static void main` | No classpath scanning, no reflection, \u003c100 ms startup |\n| **Spring Data JPA** | Raw pgjdbc + `CopyManager` | `COPY FROM STDIN` is 5-10× faster than `saveAll()` |\n| **`@Autowired`** | Avaje `@Singleton` + Lombok `@RequiredArgsConstructor` | Constructor injection only |\n| **`@ConfigurationProperties`** | `IndexerConfig` record + `System.getenv()` | Fail-fast parsing, no magic binding |\n| **`@RestController`** | Helidon SE `HttpService` functional routing | No annotations, pure function composition |\n| **`synchronized`** | `java.util.concurrent.locks.ReentrantLock` | `synchronized` pins virtual threads to carrier threads |\n| **`System.out`/`println`** | `@Slf4j` everywhere | Structured logs only |\n| **Comments/Javadoc** | Self-documenting code | If a method needs a comment, rename it |\n\n---\n\n## 🧱 Module Structure\n\n```text\nprism/                                 ← root project\n│\n├── buildSrc/                          ← Gradle convention plugins\n│   └── src/main/kotlin/\n│       ├── prism.service.gradle.kts   ← applied to main service\n│       └── prism.library.gradle.kts   ← applied to shared libs\n│\n├── prism/                             ← main service (Helidon 4 SE)\n│   └── src/\n│       ├── main/java/com/stablebridge/prism/\n│       │   ├── application/           ← inbound adapters\n│       │   │   ├── IndexerApplication.java    ← main(), wiring\n│       │   │   ├── IndexerLifecycle.java      ← shutdown hook\n│       │   │   ├── config/IndexerConfig.java  ← env → record\n│       │   │   ├── route/                     ← Helidon SE routes\n│       │   │   │   ├── HealthRoutes.java\n│       │   │   │   ├── StatsRoutes.java\n│       │   │   │   ├── TransactionRoutes.java\n│       │   │   │   ├── TransferRoutes.java\n│       │   │   │   ├── MemoRoutes.java\n│       │   │   │   ├── AccountRoutes.java\n│       │   │   │   ├── CorsConfiguration.java\n│       │   │   │   └── PaginationLimits.java\n│       │   │   ├── mapper/                    ← MapStruct\n│       │   │   └── error/GlobalErrorHandler.java\n│       │   │\n│       │   ├── domain/                ← core — zero framework imports\n│       │   │   ├── model/             ← SolanaTransaction, Account, ...\n│       │   │   ├── port/              ← TransactionStream, TransactionRepository, ...\n│       │   │   ├── service/           ← BatchService, Processor, filters\n│       │   │   ├── solana/            ← Base58, balance math, programs\n│       │   │   └── exception/\n│       │   │\n│       │   └── infrastructure/        ← outbound adapters\n│       │       ├── grpc/              ← Yellowstone stream + parser\n│       │       ├── websocket/         ← blockSubscribe stream + parser\n│       │       ├── persistence/       ← pgjdbc repositories\n│       │       │   ├── DataSourceFactory.java      ← HikariCP × 2\n│       │       │   ├── FlywayMigrator.java\n│       │       │   ├── CopyTransactionRepository.java\n│       │       │   ├── JdbcFailedTransactionRepository.java\n│       │       │   ├── JdbcMemoRepository.java\n│       │       │   ├── JdbcTransferRepository.java\n│       │       │   ├── JdbcAccountRepository.java\n│       │       │   └── JdbcStatsRepository.java\n│       │       ├── solana/Base58.java\n│       │       ├── metrics/\n│       │       │   ├── MicrometerMetricsRecorder.java\n│       │       │   └── BenchmarkLogReporter.java\n│       │       └── console/ConsoleOutputFormatter.java\n│       │\n│       └── main/resources/\n│           └── db/migration/          ← Flyway V1, V2, V3 ...\n│\n├── prism-api/                         ← shared DTOs (java-library)\n│   └── src/main/java/.../api/model/   ← Page\u003cT\u003e, TransactionResponse, ...\n│\n├── docs/\n│   ├── functional-spec.md             ← single source of truth\n│   ├── implementation-plan.md         ← phases 0-7\n│   ├── CODING_STANDARDS.md\n│   └── TESTING_STANDARDS.md\n│\n├── infra/\n│   └── prometheus.yml                 ← scrape config\n│\n├── docker-compose.yml                 ← Postgres + Prometheus + Grafana + app\n├── Makefile                           ← developer workflow automation\n├── build.gradle.kts / settings.gradle.kts\n└── CLAUDE.md                          ← agent instructions\n```\n\n---\n\n## 🚀 Quick Start\n\n### Prerequisites\n\n- 🐘 **Docker \u0026 Docker Compose** (for PostgreSQL, Prometheus, Grafana)\n- ☕ **Java 25** (for local builds)\n- 🛠️ **Make** (optional, just thin wrappers around `./gradlew` and `docker compose`)\n\n### 60-Second Onboarding\n\n```bash\n# 1️⃣ Clone\ngit clone https://github.com/Puneethkumarck/prism.git\ncd prism\n\n# 2️⃣ Boot the infrastructure (Postgres + Prometheus + Grafana)\nmake infra-up\n\n# 3️⃣ Run Prism against the free public Solana WebSocket endpoint\n#     Defaults: STREAM_MODE=websocket, RPC_WS_ENDPOINT=wss://api.mainnet-beta.solana.com\nDATABASE_URL=postgresql://indexer:indexer@localhost:5432/indexer \\\n  make run\n\n# 4️⃣ In another terminal, watch it work\ncurl -s http://localhost:3000/health | jq\ncurl -s http://localhost:3000/api/stats | jq\ncurl -s \"http://localhost:3000/api/transactions?limit=5\" | jq\ncurl -s \"http://localhost:3000/api/transfers?min_amount=1.0\u0026limit=10\" | jq\n```\n\nWithin a few seconds you'll see `[SLOT]`, `[TX]`, `[MEMO]`, and `[TRANSFER]` events streaming to stdout, and rows accumulating in Postgres.\n\n### Switching to Paid gRPC Mode\n\n```bash\nSTREAM_MODE=grpc \\\nGRPC_ENDPOINT=https://\u003cyour-yellowstone-provider\u003e.com \\\nX_TOKEN=\u003cyour-provider-token\u003e \\\nDATABASE_URL=postgresql://indexer:indexer@localhost:5432/indexer \\\n  make run\n```\n\n### Run Everything in Docker\n\n```bash\nmake up     # builds image via Jib + starts Postgres + Prometheus + Grafana + Prism\nmake down   # stops it all\n```\n\n---\n\n## 🎛️ Make Targets\n\n| Target | Description |\n|---|---|\n| `make build` | Compile + Spotless + unit + integration + ArchUnit |\n| `make test` | Unit tests only |\n| `make integration-test` | Integration tests (requires Docker for Testcontainers) |\n| `make clean` | Remove all build artifacts |\n| `make format` | Auto-format with Spotless |\n| `make lint` | Spotless check + ArchUnit (matches pre-commit hook) |\n| `make run` | Run Prism locally via Gradle |\n| `make infra-up` | Start Postgres + Prometheus + Grafana |\n| `make infra-down` | Stop infrastructure |\n| `make infra-clean` | Stop + delete volumes |\n| `make infra-status` | Show infra container status |\n| `make infra-logs` | Tail infrastructure logs |\n| `make docker-build` | Build Docker image via Jib (no Dockerfile) |\n| `make up` | Start infra + app container |\n| `make down` | Stop everything |\n| `make setup-hooks` | Point git at `.githooks/` |\n| `make help` | List all targets |\n\n---\n\n## 🌐 API Reference\n\nBase URL: `http://localhost:3000` — no authentication (v1).\nMetrics on `http://localhost:9090/metrics` (Prometheus format).\n\n### 🩺 Health\n\n```http\nGET /health\n```\n\n```json\n{ \"status\": \"ok\", \"uptime_secs\": 3600 }\n```\n\n### 📊 Stats\n\n```http\nGET /api/stats\n```\n\nUses `pg_stat_user_tables.n_live_tup` for **O(1) approximate counts** — vastly faster than `COUNT(*)` on million-row tables.\n\n```json\n{\n  \"total_transactions\": 4_812_344,\n  \"total_failed\": 1_203_111,\n  \"total_transfers\": 38_201,\n  \"total_memos\": 914_102,\n  \"total_accounts\": 87_433\n}\n```\n\n### 🧾 Transactions\n\n| Method | Path | Query Params | Notes |\n|---|---|---|---|\n| `GET` | `/api/transactions` | `limit` (default 50, max 500), `offset`, `success` (optional bool) | Paginated, `created_at DESC` |\n| `GET` | `/api/transactions/{signature}` | — | Returns `TxRow` or `404` |\n| `GET` | `/api/slots/{slot}` | — | Array, not paginated, `created_at ASC` |\n\n```bash\n# List the latest 10 successful transactions\ncurl -s \"http://localhost:3000/api/transactions?limit=10\u0026success=true\" | jq\n\n# Look up one by signature\ncurl -s \"http://localhost:3000/api/transactions/5Kx7aLm...\" | jq\n\n# All transactions in slot 312_701_542\ncurl -s \"http://localhost:3000/api/slots/312701542\" | jq\n```\n\n### 💰 Large Transfers\n\n```http\nGET /api/transfers?limit=50\u0026offset=0\u0026min_amount=10.0\n```\n\nPaginated, ordered by `amount DESC`. Threshold is configurable; default `1.0 SOL`.\n\n### 📝 Memos\n\n```http\nGET /api/memos?limit=50\u0026offset=0\n```\n\nPaginated, ordered by `created_at DESC`.\n\n### 🧍 Accounts\n\n```http\nGET /api/accounts/{pubkey}\n```\n\nReturns the most recent balance snapshot for a fee payer, or `404`.\n\n### 🚨 Error Response Format\n\n```json\n{\n  \"error\": \"transaction not found\",\n  \"status\": 404\n}\n```\n\n| Status | Meaning |\n|---|---|\n| `400` | Validation error (invalid base58, out-of-range pagination) |\n| `404` | Resource not found (signature / pubkey) |\n| `500` | Internal error (DB unreachable, etc.) |\n\n---\n\n## ⚙️ Configuration Reference\n\nEvery setting is an environment variable. `IndexerConfig` is a Java record parsed via `System.getenv()` — fail-fast on missing required vars, no binding magic, no `application.yml`.\n\n| Variable | Required | Default | Description |\n|---|---|---|---|\n| `DATABASE_URL` | ✅ Yes | — | `postgresql://user:pass@host:port/db` |\n| `STREAM_MODE` | No | `websocket` | `websocket` (free) or `grpc` (paid) |\n| `RPC_WS_ENDPOINT` | if `STREAM_MODE=websocket` | `wss://api.mainnet-beta.solana.com` | Solana WebSocket RPC URL |\n| `GRPC_ENDPOINT` | if `STREAM_MODE=grpc` | — | Yellowstone gRPC endpoint (https required except localhost) |\n| `X_TOKEN` | No | — | Auth token for Yellowstone, injected as `x-token` metadata |\n| `API_PORT` | No | `3000` | Helidon HTTP port |\n| `CONSOLE_LOG` | No | `true` | `false` or `0` suppresses `[TX]`/`[MEMO]`/`[TRANSFER]` output |\n| `BENCH_LOG` | No | `benchmark.log` | Path for 5-minute benchmark summary file |\n\n**Fail-fast validation** (in `IndexerConfig.fromEnv()`):\n- `DATABASE_URL` is always required.\n- `STREAM_MODE` must be `websocket` or `grpc` (case-insensitive).\n- `GRPC_ENDPOINT` must be a valid URI with `https` scheme (or `http` for localhost).\n- `API_PORT` must be a non-negative integer ≤ 65535.\n\n---\n\n## 📊 Observability\n\n| Layer | Technology | Details |\n|---|---|---|\n| **Metrics** | Micrometer + Prometheus | 8 counters (`indexer_tx_received`, `..._written`, `..._failed`, `..._memo`, `..._transfer`, `..._accounts_written`, `..._slots`, `..._batches`) scraped at `:9090/metrics` |\n| **Dashboards** | Grafana | Runs at `http://localhost:3001` (admin/admin), Prometheus at `http://localhost:9091` |\n| **Benchmark log** | File appender | Every 5 minutes: `timestamp | tps | recv | written | failed | failed% | memos | xfers | accts | batches | slots` |\n| **Console output** | ANSI color coded | `[SLOT]` cyan · `[TX]` white/red · `[MEMO]` magenta · `[TRANSFER]` yellow |\n| **Health** | Helidon | `GET /health` — no DB call, uptime from process start |\n\nSample benchmark log line:\n\n```text\n2026-04-11T09:17:42Z |   412 |      4120 |      2581 |      1539 |     37% |    18 |   104 |     31200 |      42 |    12\n```\n\nMainnet is a *chaotic* environment — a 37% failure rate is completely normal (bots, MEV, failed swaps). The indexer records all of it.\n\n---\n\n## 🧪 Testing Strategy\n\nThree-tier pyramid, with conventions adapted from `stablebridge-tx-recovery`:\n\n```text\n                    ┌───────────────┐\n                    │  Integration  │  Testcontainers PostgreSQL, real JDBC,\n                    │  (Docker)     │  end-to-end against both stream adapters\n                    ├───────────────┤\n                    │  Architecture │  ArchUnit: hexagonal layer rules,\n                    │  (no deps)    │  no @Autowired, no System.out, no synchronized\n                ┌───┴───────────────┴───┐\n                │      Unit Tests       │  BDD Mockito + AssertJ, no Spring context,\n                │  (single assertion)   │  fixture builders, no generic matchers\n                └───────────────────────┘\n```\n\n| Tier | Source Set | Frameworks | Docker? |\n|---|---|---|---|\n| Unit | `src/test/` | JUnit 5, BDD Mockito (`given`/`then`), AssertJ, Awaitility | No |\n| Architecture | `src/test/` | ArchUnit 1.4 | No |\n| Integration | `src/integration-test/` | JUnit 5, Testcontainers PostgreSQL, direct JDBC | ✅ Yes |\n\n**Non-negotiable testing rules:**\n\n- 🎯 **Single-assert pattern** — build expected object, then `assertThat(actual).usingRecursiveComparison().isEqualTo(expected)`.\n- 🗣️ **BDD Mockito only** — `given().willReturn()` / `then().should()`, never `when()/verify()`.\n- 🚫 **No generic matchers** — no `any()`, `anyString()`, `eq()`. Pass actual values.\n- 💬 **`// given` / `// when` / `// then` comments in every test**.\n- 🏗️ **Fixture builders** — `SOME_*` constants and `\u003cconcept\u003eBuilder()` in `src/testFixtures/`.\n- ⏱️ **Awaitility over `Thread.sleep`** — polling with timeout, not arbitrary waits.\n\nRun them:\n\n```bash\n./gradlew test              # unit + architecture (~5 s)\n./gradlew integrationTest   # integration tests (requires Docker, ~30 s)\n./gradlew build             # everything + Spotless + ArchUnit\n```\n\n---\n\n## 🗂️ Database Schema\n\nFive tables, seven indexes, one staging table. All migrations live in `prism/src/main/resources/db/migration/`.\n\n```text\n 📄 transactions            primary key: signature (varchar 88)\n    signature  varchar(88)  PK\n    slot       bigint       idx_transactions_slot\n    success    boolean      idx_transactions_success\n    created_at timestamptz  idx_transactions_created_at DESC\n\n ❌ failed_transactions\n    id         serial       PK\n    signature  varchar(88)\n    slot       bigint\n    error      text\n    created_at timestamptz  idx_failed_tx_created_at DESC\n\n 💰 large_transfers\n    id         serial       PK\n    signature  varchar(88)\n    slot       bigint\n    amount     numeric      idx_large_transfers_amount DESC\n    created_at timestamptz  idx_large_transfers_created_at DESC\n\n 📝 memos\n    id         serial       PK\n    signature  varchar(88)\n    memo       text\n    created_at timestamptz  idx_memos_created_at DESC\n\n 🧍 accounts                unique: pubkey\n    id         serial       PK\n    pubkey     varchar(88)  UNIQUE\n    lamports   bigint\n    slot       bigint\n    executable boolean\n    rent_epoch bigint\n    created_at timestamptz\n\n 🛠️ staging_transactions    (no constraints, no indexes)\n    signature  varchar(88)\n    slot       bigint\n    success    boolean       ← COPY target, truncated per flush\n```\n\n### Dual Connection Pools\n\n```text\n HikariCP Write Pool          HikariCP Read Pool\n ───────────────────          ──────────────────\n max:     20                  max:     20\n min:     5                   min:     5\n usage:                       usage:\n   COPY staging_tx              GET  /api/transactions\n   INSERT failed_tx             GET  /api/transfers\n   INSERT memos                 GET  /api/memos\n   INSERT large_transfers       GET  /api/accounts/:pubkey\n   UPSERT accounts              GET  /api/stats\n```\n\n**Why two pools?** During a mainnet burst the write pool can saturate all 20 connections for 50-100 ms. If the API shared that pool, every `GET` would sit in line behind the writes. With dedicated pools, API latency is independent of ingest load — the primary complaint about every \"indexer plus API on one DB\" setup.\n\n---\n\n## 🧠 Design Decisions, Quick Reference\n\n| # | Decision | Problem It Solves | Impact |\n|---|---|---|---|\n| 1 | **Unbounded tx queue** (`LinkedTransferQueue`) | Bounded queues cause producer block → Yellowstone `lagged` disconnect | Zero dropped transactions from backpressure |\n| 2 | **COPY FROM STDIN + staging merge** | `INSERT VALUES` is 5-10× slower for high-volume tables | 5-10× write throughput on the hottest path |\n| 3 | **200 tx / 100 ms dual-trigger batch** | Per-row writes create ~200× more DB round-trips | ~200× fewer round-trips, bounded max latency |\n| 4 | **200 acct / 2 s batch with dedup** | Per-tx account upserts spawn thousands of tasks/sec | Eliminates task churn, reduces DB pressure |\n| 5 | **Exponential reconnect** (4s→8s→16s→30s cap, 60s reset) | Thundering herd against a flapping endpoint | Progressive delay, fast recovery after stability |\n| 6 | **Dual read/write HikariCP pools** | Write bursts starve API read queries | API latency independent of ingest load |\n| 7 | **`pg_stat_user_tables` for `/api/stats`** | `COUNT(*)` on million-row tables is O(N) | O(1) approximate counts for dashboard |\n| 8 | **4 parallel writes per flush** | Sequential writes to 4 tables multiply flush latency | All 4 writes (COPY + 3 INSERTs) run concurrently on virtual threads |\n| 9 | **Helidon 4 SE (not Boot)** | Spring classpath scan, reflection, CDI → slow startup | \u003c100 ms startup, \u003c50 MB RSS, \u003c7 ms p99.999 |\n| 10 | **`ReentrantLock` (not `synchronized`)** | `synchronized` pins virtual threads to carrier | No carrier pinning, full VT throughput |\n| 11 | **MapStruct at layer boundaries** | Hand-written copy loops are bug-prone and ugly | Compile-time, type-safe, zero runtime cost |\n| 12 | **ArchUnit at build time** | Architectural rules degrade without enforcement | Hexagonal rules fail the build if violated |\n\n---\n\n## 📜 License\n\nReleased under the **MIT License**. See [`LICENSE`](LICENSE) for full text.\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n### 🔺 Prism — Refract the Solana firehose into a queryable data stream.\n\nBuilt on **Java 25 · Virtual Threads · Helidon 4 SE · pgjdbc · PostgreSQL 16**\nNo Spring Boot · No JPA · No reflection · No apologies.\n\n*Every block. Every signature. Every memo. Every time.*\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpuneethkumarck%2Fprism","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpuneethkumarck%2Fprism","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpuneethkumarck%2Fprism/lists"}