Зачем нужно знать
«ACID или BASE?» — это первый вопрос, который встаёт при выборе БД, и от ответа зависит всё: латенси, доступность, цена ошибки и сложность кода. Маркетинг любит крайности — «Postgres = надёжность», «Cassandra = scale», — но в реальности это два разных философских подхода к консистентности, оба корректные, оба применимые. Без понимания ACID и BASE инженер либо ставит Postgres везде и упирается в потолок при первых миллионах RPS, либо хватается за DynamoDB для финансов и теряет деньги на double-spend. Понимание разницы — фундамент для выбора БД, дизайна транзакций, отладки stale-read багов и разговора с архитектором на одном языке.
Mental model
ACID — это строгая дисциплина внутри транзакций: либо всё, либо ничего, инварианты не нарушаются, конкурентные транзакции изолированы, после commit данные переживут падение узла. BASE — это «мы доберёмся туда позже»: система всегда отвечает (пусть и устаревшим значением), state мягкий и сходится в фоне, eventual consistency — норма, а не баг.
Ключевая ловушка: ACID Consistency и CAP Consistency — это РАЗНЫЕ C. ACID-C говорит про инварианты БД (FK, UNIQUE, CHECK, business rules) и работает даже на single node. CAP-C — это linearizability, гарантия что все узлы видят одно и то же. Путать их — стандартная ошибка даже у senior-инженеров.
ACID и BASE — не бинарь, а спектр. Современные БД часто гибридны: Postgres + logical replication даёт eventual на read replicas; DynamoDB по умолчанию BASE, но ConsistentRead=true поднимает её до linearizable per-key; Cassandra с LOCAL_QUORUM + LWT даёт linearizability per partition; MongoDB с 4.0 умеет multi-doc ACID; Spanner вообще делает strict serializability на глобально распределённой системе (то, что в 2010 считалось невозможным).
Что показывает диаграмма
Слева — мир ACID: одна Postgres-нода с журналом транзакций. Клиент пишет через BEGIN .. COMMIT, и все операции внутри транзакции применяются атомарно. Если хоть один CHECK падает — ROLLBACK откатывает всё. Это «либо всё, либо ничего».
Справа — мир BASE: координатор Cassandra и три реплики, связанные gossip-протоколом. Клиент пишет на координатор, тот форвардит на одну из реплик и сразу отвечает «OK» (consistency=ONE). Остальные реплики догоняют асинхронно через replication и anti-entropy. Это «basically available, eventually consistent».
Один клиент работает с обоими мирами параллельно — чтобы было видно, что выбор между ACID и BASE происходит на уровне use case, а не на уровне организации. В одной системе может жить и Postgres для платежей, и Cassandra для feed.
На ноде Postgres-primary висит ADR-001 — основной trade-off: «какая ошибка дороже — двойной charge ($$$ + chargeback fee + audit) или stale post (~50ms UX-задержка)». От ответа на этот вопрос зависит выбор философии.
Сценарии и что они учат
acid-transaction — happy path транзакции. Клиент открывает BEGIN, делает три INSERT и один UPDATE inventory. Все FK и CHECK проходят, COMMIT записывает WAL fsync, ack возвращается. Учит: транзакция — это атомарный юнит, ничего из неё не видно «снаружи» до COMMIT, а после COMMIT данные durable даже при немедленном crash сервера.
acid-rollback — что происходит, когда инвариант падает. Клиент пытается заказать 5 единиц товара, но inventory.stock не хватает. CHECK constraint падает, БД сигнализирует ошибку, клиент шлёт ROLLBACK. Все четыре предыдущие операции откатываются. Никакого «полузаказа» с тремя items без четвёртого. Учит: atomicity = «всё или ничего» включает в себя и откат при ошибке, не только commit при успехе.
base-write-fast — почему BASE такая быстрая. Клиент пишет на координатор Cassandra с consistency=ONE. Координатор форвардит на ближайшую реплику (1ms), получает ack и сразу возвращает клиенту 200 OK. Остальные реплики догоняют асинхронно через 30-50ms. Учит: BASE-системы оптимизируют write latency и availability в ущерб моментальной консистентности — write возвращается за ~3ms даже если две из трёх реплик ещё не получили данные.
base-stale-then-converge — stale read и eventual convergence. Клиент пишет x=5, получает ack от node-1. Сразу делает read с consistency=ONE — попадает на node-3, которая ещё не получила update. Возвращается старое x=0. Через ~100ms gossip / anti-entropy синхронизирует node-3, retry-read возвращает x=5. Учит: stale read — это не баг BASE-систем, это их дизайн. Если клиент не может с этим жить — нужно либо повышать consistency level (QUORUM/ALL), либо использовать read-your-writes session consistency, либо переходить на ACID.
Trade-offs (развёрнутые ADR)
ADR: Выбрать ACID для платежного процессинга
- Context: Stripe-like система с charges, refunds, payouts. Цена ошибки double-charge — это chargeback fee ($15-25), audit, reputation hit, возможный fraud-разбор. Цена stale-read — пользователь видит refund на 1 секунду позже.
- Decision: Postgres + serializable isolation для core ledger. Все деньги-операции внутри транзакций. Inventory, balances, FK на user/account — всё через ACID.
- Consequences: Vertical scaling limit (~100K TPS на серьёзном железе), необходимость sharding/CDC для аналитики, дороже Cassandra на масштабе. Но deterministic correctness — и для денег это окупается всегда.
ADR: Выбрать BASE для social feed
- Context: Twitter-like timeline на 500M активных юзеров. Fan-out на 100M followers на популярного автора. ACID-транзакция «опубликовать tweet + обновить 100M timelines» не масштабируется в принципе.
- Decision: Cassandra с tunable consistency (LOCAL_QUORUM для writes на core posts, ONE для read of feeds), eventual consistency на timelines, batch fan-out workers.
- Consequences: Read-your-writes требует отдельных механизмов (например, читать свой timeline из кеша после write). Edge cases с дубликатами/потерями — компенсируются background reconciliation. Зато linearly scalable до триллионов рядов.
ADR: Гибридный подход в одной системе
- Context: SaaS-приложение: есть billing (нужен ACID), есть activity feed (BASE достаточно), есть search index (eventual OK).
- Decision: Postgres для billing, users, accounts (ACID). Cassandra/DynamoDB для activity events. Elasticsearch для search. Outbox pattern + CDC синхронизирует Postgres → Cassandra → ES.
- Consequences: Сложность инфраструктуры (3 БД вместо 1), eventual consistency между ними, нужны distributed tracing и dead-letter queues. Но каждая БД работает в своей зоне комфорта — никаких костылей.
ADR: BASE с upgrade до strong при необходимости
- Context: DynamoDB по умолчанию eventual, но в 1-2% операций (например, leader-election или idempotency token check) нужна strong.
- Decision: Default BASE для всех reads.
ConsistentRead=true для критичных операций — это удваивает RCU cost, но за тысячные доли процента запросов это не больно.
- Consequences: Нужна дисциплина — отслеживать, какие запросы requestify strong consistency, чтобы случайно не поставить везде (cost explosion). Code review должен ловить ConsistentRead=true без обоснования.
Реальные системы
- Stripe (Postgres) — pure ACID для core ledger. Все деньги-операции внутри serializable транзакций. Multi-region через Aurora-like репликацию.
- Notion (Postgres) — ACID для document state, но real-time collaboration через CRDTs (BASE-like patterns поверх ACID storage).
- Discord (Cassandra → ScyllaDB) — BASE на триллионы сообщений. Eventual consistency приемлема: пользователь видит свой message сразу (read-your-writes через локальный кеш), другие — через ~10-100ms.
- Twitter (Manhattan + Cassandra) — BASE для timelines, fan-out workers пишут eventually consistent. Critical paths (DM, billing) — ACID.
- Instagram (Cassandra + PostgreSQL) — feed и likes на Cassandra (BASE), users/auth/billing на Postgres (ACID).
- DynamoDB (Amazon) — BASE default, но multi-item ACID transactions (TransactWriteItems) доступны с 2018. Используется внутри Amazon для и того, и другого.
- Spanner (Google) — strict serializability + globally distributed. Доказывает, что граница ACID/BASE сдвигаема при правильной архитектуре (TrueTime API + Paxos).
- MongoDB — историческое BASE, с 4.0 multi-doc ACID transactions. Хороший пример эволюции «BASE → ACID без переписывания клиентов».
Anti-patterns
- Cassandra для финансов с дефолтным consistency. consistency=ONE на write + ONE на read = почти гарантированные потери транзакций при сетевых сбоях. Если уж Cassandra — то минимум QUORUM/QUORUM, а лучше LWT (lightweight transactions через Paxos).
- Postgres для логов / метрик / событий. ACID для append-only data, который никто никогда не апдейтит — это overkill. Дорого, медленно, vacuum съест всё IO. Используй ClickHouse, Cassandra, S3+Athena.
- «Mongo не ACID» как актуальный аргумент. С 4.0 multi-doc transactions, с 4.2 distributed transactions. Если архитектурное решение строится на «Mongo не умеет транзакции» — оно устарело на 7 лет.
- Confused ACID-C vs CAP-C в дизайн-документах. «Нам нужна consistency, значит выбираем CP по CAP» — а на самом деле нужны ACID-инварианты, которые работают и на single-node Postgres без всякой CAP.
- «BASE = no consistency at all». Нет. BASE даёт causal consistency, monotonic reads, read-your-writes (при правильной настройке). Просто weaker, чем linearizability.
- Использование read replicas Postgres как если бы они были linearizable. Logical/streaming replication даёт eventual consistency — те же stale reads, что в Cassandra, только без явного флага consistency.
- Distributed transactions через 2PC между микросервисами. Это попытка натянуть ACID на сервисную архитектуру. Работает плохо: блокирует ресурсы, не масштабируется, ломается при network partition. Используй Saga / outbox pattern.
Когда НЕ использовать
- ACID — когда нагрузка превышает потолок одного узла. На определённом масштабе (~100K TPS, ~10TB working set) vertical scaling упирается в железо. Тогда либо sharding (это уже не классический ACID), либо переход на BASE с компенсирующей логикой в коде.
- ACID — для append-only workloads. Логи, метрики, события, телеметрия — не требуют транзакций, не требуют CHECK constraints, не требуют изоляции. Время ACID-БД здесь тратится впустую.
- BASE — для денег, инвентаря, бронирования. Эти домены требуют negative space invariants («нельзя продать больше, чем есть»), которые BASE не умеет дать без сложных LWT/CAS поверх. Дешевле и проще взять Postgres.
- BASE — для систем с strict regulatory требованиями. Banking, healthcare, government — там аудитор требует доказательство, что не было lost writes. Eventual consistency делает доказательство очень сложным.
- BASE — без plan'а на conflict resolution. Если ты не знаешь, как будут разрешаться конкурентные writes (LWW? CRDT? application-level merge?), значит ты не готов к BASE. Default-овый LWW (last-write-wins) теряет данные, и пользователи об этом узнают.
- Strict ACID между сервисами через distributed transactions. Saga / outbox / event sourcing — современный путь. 2PC поверх HTTP-микросервисов — anti-pattern.
Дальше читать
- DDIA (Designing Data-Intensive Applications), Martin Kleppmann — Chapter 7 «Transactions» — лучшее в индустрии объяснение isolation levels, weak isolation anomalies, serializability.
- Pat Helland, «Life beyond Distributed Transactions» (ACM Queue, 2007) — манифест почему distributed ACID не масштабируется и что с этим делать.
- Eric Brewer, «CAP Twelve Years Later» (InfoQ, 2012) — отец CAP объясняет, что бинарь CP/AP — упрощение, реальность это спектр.
- System Design Primer, «SQL or NoSQL» — практический фреймворк для выбора БД.
- Discord engineering blog, «How Discord Stores Trillions of Messages» — production case study перехода на ScyllaDB (BASE на триллионы рядов).
- Spanner whitepaper (Google, 2012) — как сделать strict serializability на глобально распределённой системе через TrueTime.
- ::concept{slug="cap-theorem"} — фундамент: почему при partition приходится выбирать между C и A.
- ::concept{slug="consistency-models"} — детальная таксономия от strong до eventual.
- ::concept{slug="pacelc-theorem"} — расширение CAP: trade-off latency vs consistency даже без partition.
- ::case{slug="twitter-system-design"} — BASE для timelines на масштабе.
- ::case{slug="instagram-system-design"} — гибридная архитектура BASE + ACID.