Indexing Bitcoin’s happy path is a weekend project. The engineering is in the edges: chains reorganize and most indexers overwrite history; transactions drop out of mempools and fire-and-forget broadcasters never notice; a single RPC node fails — or, worse, silently serves the wrong chain — and takes the whole stack with it. This system is a Kotlin service suite I built to treat those edges as the primary design inputs: an indexing core with separated truth and query stores, reorg handling that preserves history instead of erasing it, event-sourced broadcasting that survives restarts, verified from-scratch cryptography underneath, and an explorer UI on top.

Role Architecture and implementation across seven modules
Stack Kotlin 1.9 / JDK 21, Spring Boot 3.2 + Spring Cloud (OpenFeign, Vault config), Cassandra, PostgreSQL + Flyway, Hibernate Search/Lucene, React 18 + TypeScript explorer
Networks Bitcoin testnet4 + a private signet devnet (Core 27.0, 3-node fleet) — fail-closed by design
Modules network (indexer), explorer, crypto, devnet, cli, shared (RPC), monitor (Prometheus + Grafana)
Status Beta; 23 Kotlin test files, Testcontainers integration suite, dedicated adversarial security tests

The problem

Two tensions define Bitcoin infrastructure, and they pull in opposite directions.

Truth versus query. A store optimized for immutable history is slow at hot lookups — address balances, UTXO selection, mempool views. A store optimized for queries invites rewriting history on reorg. One database cannot give you both properties, and pretending it can is how explorers end up showing wrong balances.

Liveness versus correctness. You want the pipeline ingesting even when downstream components fail — but never at the cost of indexing the wrong chain, double-counting a rolled-back UTXO, or losing track of a transaction you promised to broadcast.

Design decisions

Cassandra as truth, PostgreSQL as a disposable view

Blocks are fetched from bitcoind with full previous-output data (getblock verbosity 3, so fees are computed exactly, not estimated) and written once to Cassandra — the immutable source of truth, flagged utxo_indexed = false on arrival. A separate async indexer polls Cassandra every five seconds for unindexed blocks and derives the PostgreSQL query view — UTXOs with partial indexes by address, height, value, and output type; address activity; plpgsql confirmation functions with 100-block coinbase maturity — in batches of one hundred, converging within about five seconds.

The decoupling is the point: PostgreSQL downtime never blocks chain ingestion, and the query view can be dropped and rebuilt from truth at any time. The operating rule is asymmetric by design — the view is allowed to be seconds stale; the truth is never allowed to be wrong.

Reorgs mark history; they don’t erase it

                     10s poll
  bitcoind ──► block monitor ──► new tip height < last known?
                                        │
                            no ─────────┼───────── yes: REORG DETECTED
                            ▼                            │
                     sync new blocks                     ▼
                     to Cassandra              mark orphaned blocks:
                                               isOnBestChain = false
                                               orphanedAt = now          ◄── never DELETE
                                                         │
                                                         ▼
                                               roll back derived UTXOs
                                               for heights ≥ fork point
                                                         │
                                                         ▼
                                               reorg counter ++ ──► Prometheus alert
                                                         │
                                                         ▼
                                               re-sync canonical branch, replay

Six months later you can still answer “what did the chain look like before that reorg?” — which is exactly the question an auditor or an incident review asks. The monitor also handles the unglamorous realities: pruned-node recovery, initial-block-download detection, and syncing from available history when a provider can’t serve the full range.

Broadcasting as event sourcing

A submitted transaction becomes an event stream, not a fire-and-forget RPC call:

  submit ──► SUBMITTED event + snapshot ──► confirmation monitor
                     │                        tracks thresholds 1 ─► 3 ─► 6
                     ▼
             unconfirmed after 30s?
                     │ yes
                     ▼
             rebroadcast · backoff ×2.0  (30s → 60s → … → 600s cap)
                     │
                     ▼
             REBROADCAST_STATE persisted ──► restart-safe: the process
                                             forgets nothing

Event-sourced tables (track, event, snapshot) mean “did my withdrawal go through?” has a durable, inspectable answer with its full submission history — who submitted, when, how many rebroadcasts, which confirmation milestones passed.

Fail closed on the wrong chain

Each network gets primary and secondary RPC providers behind a failover client, and every provider is pinned to an expected chain verified against getblockchaininfo — testnet4 pinned as testnet4, the devnet pinned as signet. If the answer doesn’t match, the system refuses to run rather than index garbage. The same posture extends outward: watched-address activity is pushed to the MEDUSA custody backend as HMAC-signed webhooks, so downstream consumers can verify that events actually came from this system.

Cryptography from the BIPs, locked by golden vectors

A pure-Kotlin crypto module implements BIP341 Taproot key-path SIGHASH_DEFAULT (with BIP340 tagged hashes, designed to feed a threshold signer), BIP143 segwit digests, BIP32 HD derivation, and bech32 P2WPKH/P2TR address construction — each locked by test vectors matched against Bitcoin Core and rust-bitcoin. Writing the sighash algorithms from the specifications, rather than wrapping a library, is what makes the signing path auditable end to end; ECDSA currently rides on BouncyCastle, explicitly marked temporary pending a pure-Kotlin replacement.

Architecture

                       ┌──────────────────────────────────────────┐
                       │  bitcoind fleet                          │
                       │  testnet4 + private signet (Core 27.0,   │
                       │  3 nodes, 10s blocks, BIP325 signing)    │
                       └───────┬──────────────────────────────────┘
                               │ JSON-RPC · failover · chain pinning
                               ▼
        ┌─────────────────────────────────────────┐
        │  bitcoin-network (Spring Boot)          │
        │  ┌───────────────┐  ┌────────────────┐  │      ┌─────────────────────┐
        │  │ block monitor │─►│   CASSANDRA    │  │      │ broadcast subsystem │
        │  │ 10s · reorg   │  │ immutable truth│  │      │ event-sourced ·     │
        │  │ detection     │  └───────┬────────┘  │      │ backoff rebroadcast │
        │  └───────────────┘          │ 5s poll   │      │ confirmation monitor│
        │                             ▼ batch 100 │      └──────────┬──────────┘
        │                    ┌────────────────┐   │                 │
        │                    │  POSTGRESQL    │   │◄────────────────┘
        │                    │ derived UTXO / │   │
        │                    │ address view   │   │   metrics: tip height · lag ·
        │                    └───────┬────────┘   │   reorgs · provider errors ·
        └────────────────────────────┼────────────┘   watched addresses
                                     │ REST
              ┌──────────────────────┼─────────────────────────┐
              ▼                      ▼                         ▼
   ┌────────────────────┐  ┌─────────────────────┐  ┌────────────────────┐
   │ bitcoin-explorer   │  │ mempool analytics   │  │ HMAC webhooks      │
   │ OpenFeign proxy +  │  │ fee histograms ·    │  │ → MEDUSA custody   │
   │ caching · React 18 │  │ RBF (BIP125) ·      │  │   backend          │
   │ SPA in the jar     │  │ next-block template │  └────────────────────┘
   └────────────────────┘  └─────────────────────┘

Supporting cast: the mempool analytics compute fee-rate histograms, fee recommendations, replace-by-fee detection, ancestor/descendant relationships, and a simulated next-block template from the raw verbose mempool; the private signet devnet mines deterministic ten-second blocks with periodic test transactions, giving end-to-end tests a repeatable chain; a Picocli CLI covers demo and HD-wallet workflows; and a dedicated monitor module ships the Prometheus + Grafana deployment. The explorer SPA — block, transaction, address, and mempool pages with D3 visualizations, UTXO management, and a network switcher — builds into the backend jar: one artifact to deploy.

Status and limits

The stack runs on testnet4 and the private signet; mainnet operation is planned behind the same fail-closed pinning, not claimed — the mainnet network exists only as a disabled seed row. “Real-time” today means scheduled polling, REST, and outbound webhooks — WebSocket push is designed but not built. PSBT construction lives outside the crypto module (devnet block signing uses Core’s RPC), and the CLI implements demo and wallet commands with signing/multisig commands still on the roadmap. The test estate — unit tests across modules, Testcontainers integration, and an adversarial reject-path suite — is real; I list the gaps because an infrastructure page that hides them isn’t credible.

What this demonstrates

This system is distributed-systems judgment applied to hostile inputs: choosing two datastores because no one store can be both immutable and fast; treating reorgs, retries, and provider failure as designed-for flows with observable outcomes rather than exceptions; making delivery durable through event sourcing; and implementing consensus-critical cryptography from specification with vector-locked tests. If you’re building exchange infrastructure, custody rails, or anything that must stay correct while the chain misbehaves, book a consultation.