Отличный выбор. CockroachDB как раз создан для «банковской» нагрузки: строгое **SERIALIZABLE** по умолчанию, multi‑region, онлайн‑DDL. Ниже — как я бы собрал ядро процессинга (ledger) именно под CockroachDB, с нюансами, где он отличается от PostgreSQL.

# Ключевые принципы в CockroachDB

* **Изоляция**: всегда `SERIALIZABLE`. Готовь **retry‑loop** на `40001 (retry txn)` — это норма для конфликтов.
* **Локальность**: multi‑region схемы — `REGIONAL BY ROW` для транзакционных таблиц; `GLOBAL` для справочников.
* **Анти‑hotspot**: избегай монотонных ключей (sequence), используй `UUID v4/v7` и/или **hash‑sharded** индексы.
* **Блокировки**: нет advisory locks как в PG — используй `SELECT … FOR UPDATE` на «строке‑замке» (см. ниже).
* **Триггеры/расширения**: ограничены → реактивность делай через **CHANGEFEED** (CDC) в Kafka/обработчик.

---

# 1) Схема ядра (ledger)

```sql
-- Региональная настройка базы (примерно)
-- CREATE DATABASE core PRIMARY REGION eu-central SECONDARY REGION eu-north ...;

-- Справочник счетов (регион по владельцу)
CREATE TABLE ledger_accounts (
  account_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  client_id  UUID NOT NULL,
  currency   STRING NOT NULL,
  status     STRING NOT NULL,
  crdb_region crdb_internal_region NOT NULL,  -- для REGIONAL BY ROW
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
) LOCALITY REGIONAL BY ROW;

-- Неизменяемый журнал проводок
CREATE TABLE ledger_journal (
  entry_id   UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  batch_id   UUID NOT NULL,
  debit_account  UUID NOT NULL,
  credit_account UUID NOT NULL,
  amount     DECIMAL(20,6) NOT NULL,
  currency   STRING NOT NULL,
  event_time TIMESTAMPTZ NOT NULL DEFAULT now(),
  idempotency_key BYTES,                      -- уникальность на уровне батча/идемпотентности
  meta       JSONB
) LOCALITY REGIONAL BY ROW;

-- Ускоряем поиск по аккаунтам и времени + шардим индекс, чтобы не было hotspot’ов
CREATE INDEX lj_by_debit  ON ledger_journal (debit_account, event_time DESC) USING HASH WITH BUCKET_COUNT = 32;
CREATE INDEX lj_by_credit ON ledger_journal (credit_account, event_time DESC) USING HASH WITH BUCKET_COUNT = 32;
CREATE UNIQUE INDEX lj_by_idem ON ledger_journal (idempotency_key) WHERE idempotency_key IS NOT NULL;

-- Текущие балансы (одна строка на аккаунт+валюту)
CREATE TABLE ledger_balances (
  account_id UUID NOT NULL,
  currency   STRING NOT NULL,
  current_balance DECIMAL(20,6) NOT NULL DEFAULT 0,
  pending_balance DECIMAL(20,6) NOT NULL DEFAULT 0,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (account_id, currency)
) LOCALITY REGIONAL BY ROW;

-- Бизнес-объект платежа
CREATE TABLE payments (
  payment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  merchant_id UUID NOT NULL,
  status     STRING NOT NULL, -- pending/authorized/captured/refunded/failed
  amount     DECIMAL(20,6) NOT NULL,
  currency   STRING NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  meta       JSONB
) LOCALITY REGIONAL BY ROW;

CREATE INDEX p_by_merchant ON payments (merchant_id, created_at DESC) USING HASH WITH BUCKET_COUNT = 32;

-- Outbox для событий (CDC → Kafka)
CREATE TABLE outbox (
  event_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  aggregate_type STRING NOT NULL,
  aggregate_id UUID NOT NULL,
  payload JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  published_at TIMESTAMPTZ
) LOCALITY REGIONAL BY ROW;
```

**Почему так:** `REGIONAL BY ROW` держит запись ближе к пользователю/мерчанту, снижая межрегиональные WAN‑RTT. `HASH‑sharded` индексы избегают горячих ключей. Журнал — только `INSERT` (сторно отдельной проводкой).

---

# 2) Конкурентность и целостность (без overdraft’ов)

В Cockroach нет advisory‑lock’ов → делаем «строку‑замок» (или просто лочим строку баланса):

```sql
-- Псевдокод перевода средств в одной транзакции
BEGIN;

-- 1) Лочим обе строки баланса детерминированно, чтобы избежать дедлоков
--    (фиксированный порядок по UUID)
SELECT current_balance
  FROM ledger_balances
 WHERE account_id IN ($from, $to) AND currency = $cur
 ORDER BY account_id FOR UPDATE;

-- 2) Проверяем лимиты, статусы счетов, валюту и т.п.
-- 3) Пишем двойную запись в журнал (иммутабельно)
INSERT INTO ledger_journal (batch_id, debit_account, credit_account, amount, currency, idempotency_key, meta)
VALUES
 ($batch, $from, $to,  $amount, $cur, $idem, $meta),
 ($batch, $to,   $from, -$amount, $cur, $idem, $meta);

-- 4) Обновляем балансы, не допуская отрицательных
UPDATE ledger_balances
   SET current_balance = current_balance - $amount, updated_at = now()
 WHERE account_id = $from AND currency = $cur
   AND current_balance >= $amount;

-- Если rowcount = 0 → недостаточно средств → ROLLBACK

UPDATE ledger_balances
   SET current_balance = current_balance + $amount, updated_at = now()
 WHERE account_id = $to AND currency = $cur;

-- 5) Пишем событие во внешний мир через outbox
INSERT INTO outbox (aggregate_type, aggregate_id, payload)
VALUES ('payment', $payment_id, jsonb_build_object(...));

COMMIT;
```

**Идемпотентность:** клиент всегда шлёт `Idempotency-Key`; на конфликте `lj_by_idem` возвращаем уже созданный результат.

**Важно для CRDB:** оберни это в **retry‑loop** — если получишь `40001`, повтори транзакцию (это нормальный рабочий режим при конкуренции).

---

# 3) Multi‑region дизайн

* Таблицы с интенсивной записью: **`REGIONAL BY ROW`**; колонку `crdb_region` проставляй по мерчанту/аккаунту.
* Справочники (валюты, MCC): **`GLOBAL`** — чтения быстрые во всех регионах.
* Критические чтения «здесь и сейчас» — по месту записи; отчётные/аналитические чтения могут использовать **Follower Reads / bounded staleness** для разгрузки.
* **Survival goal**: на уровне базы/таблиц выставляй устойчивость к падению региона.

---

# 4) Масштабирование и партиционирование

* CRDB сам шардингует на уровне KV‑диапазонов; ты задаёшь **partition by region** и **hash‑sharded** индексы.
* Для огромного журнала — **партиции по времени** (месяц) + вторичная локализация по региону.
* Избегай крупных транзакций и долгих `SELECT` с `FOR UPDATE` — держи транзакции короткими.

---

# 5) CDC, очереди, интеграции

* **CHANGEFEED → Kafka / cloud storage** из `outbox` и/или `ledger_journal`:

  * ключ по `entry_id`/`event_id`,
  * опции: маркеры «резолва» для exactly‑once в потребителе,
  * downstream — вебхуки, AML, фрод, бухгалтерия.
* Идемпотентность консьюмеров — по `event_id`.

---

# 6) Надёжность и бэкапы

* **Scheduled Backups**: инкрементальные в объектное хранилище (S3/GCS), **PITR**.
* On‑line DDL безопасен, но проверяй планы (statement statistics, EXPLAIN ANALYZE).
* Наблюдаемость: встроенные графики + экспортер метрик в Prometheus/Grafana.

---

# 7) Что учесть vs PostgreSQL

* Нет/ограничены: **триггеры/расширения** уровня PG → логику выносим в приложение/CDC.
* **Advisory locks нет** → используй `SELECT … FOR UPDATE` на строках‑«замках».
* Избегай **монотонных** PK/sequence (горячие ключи) — UUID/временные UUID + hash‑sharded.
* Поддержка `UPSERT`, `INSERT ON CONFLICT` есть; но некоторые PG‑спец‑фичи могут отличаться.
* Готовь системно **retry‑loop** для всех записывающих транзакций.

---

# 8) Мини‑чеклист конфигурации

* Типы денег: `DECIMAL(20,6)` в БД; в приложении можно хранить в «центах» как `BigInt`.
* Время: `TIMESTAMPTZ`, всё в **UTC**.
* Балансы: строгий инвариант `current_balance >= 0` через `WHERE` + проверка rowcount.
* Индексы: на ключевых путях запросов, **HASH‑sharded** на горячих колонках.
* Таблицы: `REGIONAL BY ROW` для OLTP; `GLOBAL` для справочников.
* CDC: changefeed из `outbox`; консьюмер — идемпотентный.
* Транзакции: короткие, с retry‑loop на `40001`.

---

Если хочешь, под твои реальные **TPS и регионы** (например: EU‑Riga + DE‑Frankfurt) я соберу готовые DDL с `LOCALITY`, пример **CHANGEFEED** в Kafka, шаблон **retry‑loop** (Node.js/TS и Go/Java), и схему партиционирования журнала на 12–24 месяца хранения.
