StackPractices
intermediate By Mathias Paulenko

ACID vs BASE — Consistency Models Explained

A practical guide comparing ACID and BASE consistency models: when to choose strong consistency and when to accept eventual consistency.

ACID and BASE are two answers to the same question: who enforces correctness when data lives in more than one place — the database, or your application code? This guide covers what each model guarantees, what it costs, and how to pick for a real system.

Overview

ACID guarantees strong consistency through transactions that are Atomic, Consistent, Isolated, and Durable — the database rejects anything that would leave data invalid. BASE (Basically Available, Soft state, Eventually consistent) accepts temporary inconsistency in exchange for availability and horizontal scale, pushing correctness work into the application.

Neither is “better”. ACID pays a coordination cost upfront for immediate correctness; BASE skips coordination and pays it back later as conflict-resolution code. Picking wrong in either direction is expensive — ACID everywhere stalls under load, BASE on financial data loses money.

For context: Eric Brewer — of CAP theorem fame — coined BASE in a 2008 ACM Queue paper as NoSQL stores like Dynamo and Cassandra scaled past what coordination could afford. The acronym is a chemistry joke, but the trade-off is real: relax the guarantees an “acid” gives, and the system stays responsive when parts of it are unreachable.

ACID Properties

Atomicity

All operations in a transaction complete successfully, or none do. There’s no partial completion:

BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE id = 'B';
COMMIT;  -- Both succeed, or ROLLBACK cancels both

If the second UPDATE fails, the first one is rolled back — account A doesn’t lose 100 into the void.

Consistency

Transactions move the database from one valid state to another, preserving constraints and rules. A transfer can’t violate balance >= 0 or break a foreign key, no matter what else is running.

Isolation

Concurrent transactions don’t interfere with each other; the result is as if they ran sequentially. How strictly this holds depends on the isolation level — see the table below.

Durability

Once committed, changes survive crashes. The write hits the WAL (or equivalent) before the client gets a confirmation, so a restart replays it rather than losing it.

That guarantee is also the bill. Every commit pays for it: fsync to the write-ahead log, lock manager bookkeeping, and MVCC versions kept around for readers. That’s the coordination cost BASE is avoiding — small on a single node, painful when the same guarantees have to span replicas and regions.

Isolation Levels

Isolation is where ACID stops being absolute. Lower levels trade correctness for concurrency:

LevelDirty ReadNon-Repeatable ReadPhantom ReadUse Case
Read UncommittedPossiblePossiblePossibleRare, analytics only
Read CommittedNoPossiblePossibleDefault for most databases
Repeatable ReadNoNoPossibleFinancial read operations
SerializableNoNoNoCritical financial transactions

Two practical notes: “default for most databases” hides real differences — PostgreSQL defaults to Read Committed, MySQL InnoDB to Repeatable Read. And Serializable isn’t free: it means lock contention or serialization failures your code must retry.

A phantom read is the subtle one — it slips past Repeatable Read because the anomaly isn’t a changed row but a new row matching your predicate. Query SELECT COUNT(*) FROM orders WHERE status = 'pending' twice inside one transaction and a concurrent insert can change the answer. Only Serializable (or explicit predicate locking) prevents it.

BASE Properties

Basically Available

The system guarantees a response to every request — even under partial failure. That response may be stale or incomplete, but it arrives. The trade shows up during a node failure: where a serializable ACID store would time out or refuse the read, a BASE store answers with whatever it has.

Soft State

The state may change over time without new input, as replicas catch up and reconcile. Reading the same key twice can return different values.

Eventually Consistent

If writes stop, all nodes converge to the same value. How long “eventually” takes depends on replication lag — typically milliseconds, but unbounded under network problems:

flowchart diagram: Write X = 42

During the replication window, a read hitting Replica B returns null. Whether that matters is the whole ACID-vs-BASE question — a social feed absorbing that window is invisible to users, while an inventory count absorbing it oversells stock.

ACID vs BASE Comparison

AspectACIDBASE
ConsistencyStrong (immediate)Eventual (delayed)
Availability under partitionSacrificed — the write is rejectedPreserved — stale reads accepted
CoordinationConsensus/locks at commit timeNone at commit; reconciliation later
Invariant enforcementDatabaseApplication code
Failure handlingAutomatic rollbackCompensating transactions, retries
Best forFinancial, inventory, bookingsSocial, analytics, caching, sessions
Example systemsPostgreSQL, MySQL (InnoDB)Cassandra, DynamoDB, Couchbase

CAP Theorem

The CAP theorem says a distributed system can guarantee at most two of Consistency, Availability, and Partition Tolerance. Since partitions happen whether you like it or not, the real choice is CP vs AP — what the system does during a partition:

  • CP (ACID-style): refuse writes it can’t coordinate. Correct, but unavailable.
  • AP (BASE-style): accept writes anyway and reconcile later. Available, but temporarily inconsistent.

The CAP theorem guide covers the edge cases — including why “2 out of 3” is a simplification (it’s really about behavior under partition, not a feature menu).

The sharper version is PACELC: if there’s a Partition, choose between Availability and Consistency; Else, choose between Latency and Consistency. Even without any partition, synchronous replication means every write waits for another node — BASE systems trade that wait for speed. CAP describes the failure mode; PACELC describes the steady state too.

The Real Question: Who Owns the Invariant?

The deciding dimension isn’t scale or speed — it’s who enforces the invariants:

  • Database-owned invariants → ACID. If correctness means “the sum of these two accounts never changes” or “no seat gets sold twice”, the invariant spans entities and must be enforced atomically. That’s exactly what ACID transactions do.
  • Application-owned logic → BASE is viable. If operations are commutative or idempotent (add item to cart, increment a counter, append to a feed) and stale reads are tolerable, the application can reconcile — and you get availability and write throughput in exchange.

There’s a cost asymmetry worth internalizing: cross-partition ACID pays consensus on every commit (a quorum round-trip, or 2PC across systems — two 99.9%-available services joined by 2PC drop to ~99.8% combined). BASE pays that same complexity later, as conflict-resolution code, idempotent handlers, and reconciliation jobs you have to write and maintain.

Choosing Between ACID and BASE

Choose ACID When

  • Financial transactions (banking, payments, trading) — the invariant is the money
  • Inventory management — overselling is a correctness bug, not a UX bug
  • Booking systems — double-booking can’t be reconciled after the fact
  • Regulatory requirements demand exact, auditable records
  • The cost of inconsistency exceeds the cost of rejecting a request

Choose BASE When

  • Social feeds and activity streams — a slightly stale feed is fine
  • Analytics, metrics, recommendations — approximate is sufficient
  • Shopping carts and sessions — mergeable, recoverable state
  • Content delivery and caching — staleness is the design, not a bug
  • Leaderboards, view counters, reaction counts — approximate numbers are the product
  • High write throughput where losing writes is worse than delaying consistency

Tunable Consistency

ACID vs BASE isn’t strictly binary. Cassandra, DynamoDB, and Cosmos DB let you pick consistency per operation:

  • ONE — read/write a single node. Fastest, weakest guarantee.
  • QUORUM — a majority must respond. With replication factor N=3 and QUORUM on both sides, you get R + W > N — reads and writes overlap, so a read sees the latest acknowledged write.
  • ALL — every replica must respond. Maximum consistency, availability of the slowest node.

The rule of thumb: R + W > N gives you strong consistency for that operation at quorum cost; R + W <= N is faster but can return stale data. Use QUORUM for the operations that feed decisions (checkout, balance checks) and ONE for the rest (view counts, impressions).

Cosmos DB exposes the spectrum more explicitly with five levels: Strong (linearizable), Bounded Staleness (old by at most K versions or T time), Session (read-your-writes within a session), Consistent Prefix (ordered but possibly lagging), and Eventual. That middle ground — Bounded Staleness and Session — is often the practical sweet spot: strict enough for UX, cheap enough for scale.

BASE Read Anomalies and How to Handle Them

Eventual consistency has named failure modes — handle them explicitly rather than discovering them in production:

  • Read-your-writes violation — a user updates their profile, refreshes, and sees the old value because the read hit a lagging replica. Mitigate with sticky sessions (route reads to the node that took the write), read repair, or storing a version token and checking it on read.
  • Non-monotonic reads — values appear to go backwards as reads bounce between replicas at different lag. Mitigate with monotonic-read guarantees (session tokens in Cosmos DB, readConcern: "majority" patterns) or pinning a session to one replica.
  • Lost updates on concurrent writes — two writes race; last-write-wins silently drops one. Mitigate with vector clocks or application-level merge logic (a cart that unions items instead of replacing them).

Writes have their own failure mode during a partition: split-brain. Two nodes accept conflicting writes for the same key and both “win” locally until the partition heals. Everything downstream — inventory counts, feature flags, prices — is now forked, and resolving it requires either a deterministic merge rule or human intervention. If your domain can’t express “merge two conflicting values” safely, that data doesn’t belong in BASE.

If your use case can’t tolerate any of these even under partition, that’s the signal to go ACID for that data.

Hybrid Approaches

Most real systems split by data criticality — ACID at the core, BASE at the edges:

flowchart diagram: subgraph App[Application Layer

The split follows the invariant rule: whatever the database must enforce stays ACID; everything the application can reconcile goes BASE. Database replication covers how replica lag creates the BASE window in the first place.

Implementing BASE with Sagas

When you need BASE scale but ACID-like reliability, sagas split a distributed transaction into local transactions with compensating actions on failure:

class OrderSaga {
  async execute(order: Order): Promise<void> {
    try {
      await this.inventoryService.reserve(order.items);
      await this.paymentService.charge(order.total);
      await this.shippingService.schedule(order);
    } catch (error) {
      await this.compensate(order);
      throw error;
    }
  }

  private async compensate(order: Order): Promise<void> {
    await this.inventoryService.release(order.items);
    await this.paymentService.refund(order.total);
  }
}

Two requirements make sagas work: every step needs an idempotent compensation (retrying release twice must be safe), and you need the outbox pattern for event publication — writing the order and its event in the same local transaction, then publishing asynchronously. Idempotency keys matter at every hop: a retried charge without a deduplication key is a double charge, which is exactly the kind of invariant you moved into application code.

One design rule for the step order: put non-compensable actions last. A “send confirmation email” step can’t be un-sent, so it runs only after everything that could trigger compensation has succeeded — or it queues behind the final commit rather than inside the saga.

The distributed lock pattern is the adjacent tool when you need exclusive access across services instead of compensation.

Hybrid E-commerce Scenario

A concrete split for a system doing 500K orders/day:

ServiceModelStoreConsistency
PaymentsACIDPostgreSQLSerializable
InventoryACIDPostgreSQLRepeatable Read
OrdersACIDPostgreSQLRead Committed
CatalogBASEMongoDBEventual
SearchBASEElasticsearchNear-real-time
AnalyticsBASEClickHouseEventual

The order flow uses a saga: reserve inventory (ACID) → charge payment (ACID) → create order (ACID) → publish OrderCreated via outbox (BASE, Kafka). A payment failure compensates by releasing the reservation; an order failure refunds and releases.

Sync between stores is deliberately lagged — catalog changes reach Elasticsearch via change streams (1-5 s), orders reach analytics via Kafka consumers (30-60 s). The mitigations table is the part teams forget: stale catalog → TTL cache plus refresh; unindexed review → scheduled reindex; analytics behind → accept NRT; saga compensation failure → alert plus a daily reconciliation job that cross-checks the stores.

The lessons that survive contact with production: ACID for money, BASE for everything else; the outbox solves the dual-write problem; compensations must be idempotent; and periodic reconciliation is what catches the silent inconsistencies nobody alerted on.

Decision Checklist

Walk this list per dataset — not per system:

QuestionPoints to
Does a wrong answer corrupt money, stock, or bookings?ACID
Can two replicas see different values during a partition?BASE only if yes is acceptable
Can conflicting writes be merged deterministically?BASE viable
Do users act on the value they just wrote?BASE needs read-your-writes handling
Is the data append-only (logs, events, metrics)?BASE fits naturally
Does a regulator require exact records?ACID
Does a single operation span multiple entities?ACID transaction

If three or more answers point to ACID for one dataset, don’t fight it — keep that data in PostgreSQL and let the edges go BASE.

Common Mistakes

  • Using ACID for everything — serializable transactions on non-critical data buy latency and contention you didn’t need.
  • Using BASE for financial data — eventual consistency on balances is how double-spends and overselling happen.
  • Ignoring the CAP choice — “our system is consistent AND available” is only true until the first partition; decide what happens during one.
  • Not handling BASE read anomalies — stale reads driving writes (charge based on a lagging balance read) is the classic production incident.
  • Treating sagas as free ACID — compensation code is application-owned correctness; if you skip it, you have BASE with extra steps and none of the safety.
  • One consistency level for all operations — with tunable consistency, ONE on a balance read and ALL on a view counter are both wrong; map the level to the operation’s tolerance for staleness.
  • No reconciliation job — eventual consistency converges replicas, not business invariants; something has to periodically verify that orders match payments.
  • Assuming “eventual” means seconds — replication lag is unbounded during partitions or overload. If nobody alerts on lag, “eventually” quietly becomes “tomorrow”.
  • Not measuring replication lag — in a BASE system, lag is a first-class metric. You can’t reason about staleness you’re not measuring.

See Also

Frequently Asked Questions

Can a database support both ACID and BASE?

Yes — and many do. PostgreSQL gives ACID on the primary while read replicas serve effectively BASE results. Cosmos DB exposes five consistency levels you pick per operation, and Cassandra lets you set ONE/QUORUM/ALL per query. The model is often per-workload, not per-database.

How do I handle conflicts in BASE systems?

Three common strategies: last-write-wins with timestamps (simplest, can lose data), vector clocks (detects concurrent writes, app resolves), or designing operations to be commutative — a cart that merges items never has a conflict. The best option is usually making the conflict impossible rather than resolving it.

Is BASE faster than ACID?

For writes under load and across regions, yes — no coordination round-trip at commit time. But BASE shifts cost to the application: idempotent handlers, conflict resolution, and reconciliation jobs. You trade database time for engineering time.

What does tunable consistency actually give me?

The ability to pay for consistency only where it matters. With replication factor 3, QUORUM reads and writes overlap (R + W > N), so a quorum read sees the latest quorum write. ONE is fast but may be stale. The mistake is using one level for everything.

How do I stop users from seeing their own stale writes?

Use read-your-writes guarantees: sticky sessions routing reads to the replica that took the write, session tokens (Cosmos DB supports this natively), or writing then reading back through a quorum read until convergence. A cheap alternative is optimistic UI — show the new value client-side while replication catches up.

Which model should I pick for a payments system?

ACID, without hesitation — payment invariants (a charge exactly once, balance never negative) must hold even during partitions. The parts around payments (notifications, analytics, receipts display) can be BASE. Draw the boundary at the invariant, not at the service.

Can I use event sourcing instead of choosing?

Event sourcing is complementary, not a substitute — it records an append-only event log but still needs a consistency model for projections and reads. It's often paired with BASE: events write fast, projections converge eventually. The CQRS + event sourcing guide covers the combination.