ACID vs BASE
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 17 min · 5 figures

    ACID vs BASE

    Two ways to keep data correct: ACID transactions that guard invariants inside the database, and BASE designs that accept temporary inconsistency for availability and scale. The history, the spectrum between them, BASE patterns and how to choose per feature.

    ACID and BASE are two answers to one question: when a change touches several pieces of data, who keeps them consistent? With ACID, the database does: a transaction’s writes commit together or not at all, isolated from other transactions, so your invariants hold at every commit. With BASE (Basically Available, Soft state, Eventually consistent), the application does: it splits the change into small local steps, lets copies disagree for a while, and repairs the gap with retries, compensations and reconciliation, in exchange for availability and scale.

    Picture it first: the teller and the cheque drop box

    At a bank teller’s window you hand over cash, and the teller updates the ledger and stamps your passbook before you leave. Both records change or neither does, and nobody sees a half-finished deposit. If the queue is long, you wait.

    Next to the window is a cheque drop box. You drop the cheque, take a printed receipt and leave in ten seconds. Your banking app shows the amount as “uncleared”. Over the next day the bank presents the cheque to the other bank and either credits you or, if it bounces, posts a reversal entry and sends you an SMS. At night a clerk matches every receipt against what actually cleared.

    The drop box never makes you queue, and the bank still ends up correct; it just needs more machinery.

    Map it back: the teller is an ACID transaction. The receipt is a durable record of intent, like an outbox row. “Uncleared” is soft state that users can see. The reversal entry is a compensation, and the nightly match is a reconciliation job. Together, those are BASE.

    Two philosophies and where they came from

    ACID: the database makes a group of writes atomic, isolated and durable, so invariants such as “every debit has a matching credit” hold whenever a transaction commits (basics in ACID transactions).

    BASE: the system stays basically available when parts fail, accepts soft state that changes as messages land, and becomes eventually consistent once they are processed. The application owns every invariant that spans steps.

    Eric Brewer introduced the pair in his July 2000 PODC keynote, the talk that also stated the CAP conjecture. His slide set ACID (strong consistency, isolation, pessimistic, hard to evolve) against BASE (stale data is fine, availability first, approximate answers, optimistic, simpler and faster), then added: “But I think it’s a spectrum.” Real internet systems, he said, are “a careful mixture” of both; Inktomi kept ACID for user profiles and for the logging that billed revenue.

    Dan Pritchett of eBay turned the slogan into engineering practice in “BASE: An Acid Alternative” (ACM Queue, 2008). His case starts with availability arithmetic. A two-phase commit across two databases needs both of them up, so its availability is the product of theirs:

    0.999 × 0.999 ≈ 0.998, which is about 43 extra minutes of downtime a month (0.1% of a 43,200-minute month).

    Chain five such dependencies and you get 0.999⁵ ≈ 0.995, or about 3.6 hours of downtime a month. BASE instead splits data by function (users, items and transactions in separate databases), shards each function, and shapes operations so a failed database hurts only its slice: with users spread over five servers, one failure affects 20% of them. The rest of his article introduces patterns you will meet below: a queue committed in the same database as the data, consumers that record which updates they have applied, and updates whose order doesn’t matter.

    Two different kinds of “consistent”

    The word does different jobs in the two acronyms, and mixing them up is a common interview slip.

    • ACID’s C is about invariants. A transaction moves the data from one valid state to another. The database enforces the rules you declare (primary keys, foreign keys, CHECK (balance >= 0)), and atomicity plus isolation keep the rules your code enforces safe from crashes and races.
    • BASE’s E is about convergence. If writes stop, every copy (replicas, caches, derived tables, search indexes) eventually shows the same value. It promises nothing about how long that takes, or about rules that span several records.
    • CAP’s C is linearizability: a read that starts after an acknowledged write sees it. That is a third idea again; see CAP theorem and PACELC, and the levels in between in strong vs eventual consistency.

    These axes are independent. A PostgreSQL primary with asynchronous read replicas gives you ACID transactions and eventually consistent reads. A key-value store can make every single-key write linearizable and still offer no way to keep two keys in step. So don’t ask “are we ACID or BASE?”; ask which invariant must hold, across which records, and how stale each read may be.

    “Basically available” and “soft state”, decoded

    “Basically available” means a partial failure degrades a slice of the system rather than all of it: one shard is down, so 20% of users see an error while 80% carry on. “Soft state” means what you see can change without new user input, because in-flight messages are still landing. A restaurant’s earnings total that ticks up two seconds after a sale is soft state.

    SQL vs NoSQL is no longer the dividing line

    Fifteen years ago, “ACID” meant a relational database and “BASE” meant a NoSQL store. Both camps have since moved toward the middle.

    NoSQL stores added transactions.

    • DynamoDB’s TransactWriteItems groups up to 100 actions on up to 100 distinct items, across tables in one account and Region, at most 4 MB in total, all-or-nothing and serializable against ordinary single-item writes. It is not free: every item is written twice (prepare, then commit), so a transaction costs double the write capacity, and its changes reach global secondary indexes and streams gradually, not atomically. In global tables the guarantee holds only in the Region where you wrote; other Regions can briefly see half a transaction.
    • MongoDB has always made a write to one document atomic. Multi-document transactions arrived in 4.0 for replica sets and 4.2 for sharded clusters, and the manual warns they cost more than single-document writes and are no substitute for good schema design.
    • Cassandra offers lightweight transactions: a Paxos-based compare-and-set inside one partition, at several round trips per write instead of one (details in quorums and leaderless replication).

    Distributed SQL made ACID span machines. Spanner runs serializable, externally consistent transactions across regions, ordered by TrueTime timestamps. CockroachDB defaults to SERIALIZABLE, and clients must retry transactions that fail with SQLSTATE 40001. YugabyteDB offers serializable, snapshot and read committed isolation across nodes. You pay with consensus round trips on every commit and retries under contention.

    And SQL shops run BASE every day. Reads from an asynchronous replica can be stale, a cache is eventually consistent by construction, and a search index fed by change data capture lags by seconds. Once a company splits into services that each own a database, any workflow crossing two of them is a saga, even if every database is PostgreSQL.

    So the label belongs to an operation, not a product.

    The spectrum from one-node ACID to pure BASE. Each column shows what one transaction can span, example systems, and the main cost.Notice Moving right never removes the invariants. It moves the job of guarding them from the database into your code.
    One-node ACID Distributed ACID ACID per aggregate, BASE across Pure BASE
    One transaction can span Any rows on one primary Any rows on any node One item, document or service One key
    Cross-record invariants guarded by The database The database Database inside, application across The application
    Commit cost One local log flush Consensus round trips; tens of ms across regions One local commit per step Local write, or none
    When a node or link fails Writes stop until failover The minority side stalls Each service degrades on its own Every replica keeps answering
    You pay with A ceiling on writes Latency, retries on 40001 Sagas, outbox, idempotency Conflicts, reconciliation, visible staleness
    The spectrum, compared. Moving right buys availability and scale; the bill arrives as application code.

    The same money movement, two ways

    Take Pritchett’s marketplace example in a food-delivery shape. When an order is delivered, you record the sale and add its amount to the restaurant’s running earnings total, which the restaurant app shows on a dashboard.

    The ACID version keeps both tables in one database and wraps both writes in one transaction:

    BEGIN;
    INSERT INTO sales (sale_id, restaurant_id, amount) VALUES (9001, 42, 640);
    UPDATE restaurant_earnings SET total = total + 640 WHERE restaurant_id = 42;
    COMMIT;

    It is simple and correct, but it pins sales and restaurant_earnings to one server forever, and every sale at a busy restaurant queues on the same row lock.

    The BASE version treats the running total as what it really is, a cache of the sales table, and lets it lag. The sales service commits the sale and an event in one local transaction (the transactional outbox), a relay publishes the event, and the earnings service applies it at most once:

    -- Earnings DB, for each delivered event (PostgreSQL)
    WITH first_time AS (
      INSERT INTO applied_sales (sale_id) VALUES (9001)
      ON CONFLICT (sale_id) DO NOTHING
      RETURNING sale_id
    )
    UPDATE restaurant_earnings SET total = total + 640
    WHERE restaurant_id = 42 AND EXISTS (SELECT 1 FROM first_time);

    A redelivered event finds its sale_id already there, so the update touches nothing; the consumer acknowledges the message only after this commits.

    Recording a sale and updating a running total. Top: one ACID transaction in a shared database. Bottom: Pritchett-style BASE with a local outbox commit, a queue and an idempotent consumer.Notice In the BASE version neither database waits for the other, so either one can be down without blocking new sales.

    You gain independent databases on independent hardware, and a sale never blocks on the earnings service. You pay with a dashboard total that is soft state for a second or two, plus a relay, a queue, an applied_sales table and a plan for events that keep failing.

    Pritchett also handled ordering. If the earnings row keeps a “last sale at” column and events can arrive out of order, write the update so the value can only move forward:

    UPDATE restaurant_earnings
    SET last_sale_at = GREATEST(last_sale_at, TIMESTAMPTZ '2026-10-27 20:14:05+05:30')
    WHERE restaurant_id = 42;

    Now any arrival order gives the same result, and the queue doesn’t need expensive strict ordering.

    BASE patterns that make it safe

    BASE is not “skip transactions and hope”. Every BASE design that survives production mixes the patterns below, and each has a price.

    Idempotency keys. The client sends one unique key per logical operation with every retry; the server stores the key with the result and replays that result for duplicates. Stripe’s API takes an Idempotency-Key header on its POST endpoints, and DynamoDB de-duplicates TransactWriteItems calls with the same client token for 10 minutes. You gain safe retries after ambiguous failures such as a timeout; you pay with a key table and a retention window. See idempotency.

    Outbox plus idempotent consumers. Commit the business row and the event row together, publish afterwards, and accept that delivery is at least once. Every consumer then records what it has applied (the applied_sales table) or makes each write naturally idempotent: SET status = 'PLACED' rather than total = total + 640. See effectively-once processing and the inbox pattern.

    Sagas with compensations. Hector Garcia-Molina and Kenneth Salem defined a saga in 1987: a long transaction broken into a sequence of smaller ones, each paired with a compensating transaction that undoes it semantically. A compensation doesn’t restore old bytes, because other work may have run in between: cancelling a seat booking releases one seat; it does not write back the old seat count. Sagas also give up isolation, since other transactions can see a saga’s partial results. The trade against two-phase commit is in distributed transactions: 2PC vs sagas.

    Commutative, order-independent updates. Increments, GREATEST on timestamps and set unions give the same result in any order. CRDT-style counters go further: each replica increments only its own slot, a merge keeps the larger value per slot, and the count is the sum of the slots, so replicas accept likes independently and still converge (see clocks, ordering and conflict resolution).

    Versioned or conditional writes. UPDATE carts SET items = :items, version = 8 WHERE cart_id = :id AND version = 7, or a DynamoDB condition expression, rejects a write computed from stale data instead of silently overwriting a newer one; see optimistic vs pessimistic locking.

    Reconciliation jobs. A scheduled job compares two sources that should agree, such as your payment ledger against the provider’s settlement file, and repairs or flags the difference. It catches the bugs every other pattern missed, so give it an owner and an alert, not just a cron entry.

    Pending states the user can see. A UPI app shows “processing” instead of pretending a payment has either worked or failed; a food app shows “waiting for the restaurant to confirm”. Naming the in-between state turns an inconsistency into a status that support staff can look up.

    The dual write is the classic BASE bug

    “Commit to the database, then publish to Kafka” looks harmless. If the process dies between the two, the order exists and nobody hears about it. If you publish first and the commit then fails, consumers act on an order that never existed. Put the event in the same local transaction (outbox) or derive it from the database log (change data capture).

    Worked example: placing a food-delivery order

    A customer orders biryani. Four parties take part: the order service, the payment provider (a UPI or card gateway), the restaurant and the rider dispatch system. The skill is to draw the ACID boundary tightly around the facts that must never disagree and make everything else a BASE edge.

    What must be atomic? Money and order state: never a captured payment without a placed order, or a placed order without its payment record. Both live in the order service’s database and change together.

    What can lag? Telling the restaurant, finding a rider, push notifications, search and analytics. If one is a few seconds late nobody loses money, and if one is down, orders should still be accepted.

    1. T1, place order. One local transaction inserts the order (AWAITING_PAYMENT) and a payment intent carrying an idempotency key. The app opens the UPI payment screen.
    2. T2, payment succeeds. The provider’s callback (a webhook, which may arrive twice) runs one transaction: insert the double-entry ledger lines, set the order to PLACED, insert an OrderPlaced outbox row. A unique constraint on the provider’s transaction ID turns a duplicate callback into a no-op.
    3. BASE edges. The relay publishes OrderPlaced. The restaurant service rings the tablet and retries until it is acknowledged, dispatch searches for a rider and escalates if none is found in 10 minutes, and notifications de-duplicate by event ID.
    Order placement split into an ACID core and BASE edges. T1 and T2 are single-database transactions; T2’s outbox row is relayed to a queue that feeds the restaurant, dispatch and notification consumers.Notice The customer only ever waits on the two boxes inside the ACID core. Everything on the right can fail and retry without touching money.

    The failure path: the restaurant rejects

    Now suppose the restaurant doesn’t accept within 3 minutes because it has run out of biryani. This is where BASE earns its keep or shows its cost. The order service runs the saga’s compensations:

    • C1, one local transaction: set the order to CANCELLED, write reversal ledger lines (new rows; the originals are never deleted) and insert a RefundRequested outbox row.
    • C2, a refund call to the provider with its own idempotency key, retried with backoff until the provider confirms. Until then the app says “refund initiated”.
    The compensation path as a timeline. The restaurant never accepts, so the order service cancels and reverses in one local transaction, then retries the refund until the provider confirms.Notice The two highlighted states in the top lane: the inconsistency is not hidden, it is shown to the customer as a named status.

    Do the arithmetic before you call this an edge case. At, say, 3 million orders a day and a 0.5% rejection-or-timeout rate, 3,000,000 × 0.005 is 15,000 compensations a day, and each must refund exactly once. That path needs the same tests, dashboards and on-call ownership as the happy path.

    What BASE costs you

    BASE moves work from the database to your team. Budget for three kinds of cost.

    Anomalies users can see.

    • Overselling: if an asynchronous consumer decrements stock, the 101st buyer of 100 units is confirmed before the counter catches up. Reserve stock with one atomic conditional update in one place.
    • Double notifications: at-least-once delivery without de-duplication sends “your order is on the way” twice.
    • Stale balances: a wallet screen reading a replica shows the old balance right after a top-up, so the user tops up again. Route a user’s reads to the primary for a few seconds after they write (read-your-writes; see data replication).

    Engineering effort. Every step needs a timeout, retries with backoff, an idempotency story, a compensation, and a plan for when the compensation itself fails (usually: keep retrying, then page a human). Each path needs a test that injects the failure.

    Observability of in-flight work. An ACID transaction is either committed or not; a saga can sit half-done for hours. Track the oldest unpublished outbox row, consumer lag, orders stuck in each intermediate state past its expected maximum, and mismatches per reconciliation run. An alert on “orders in AWAITING_RESTAURANT for over 10 minutes” catches a broken consumer long before customers call.

    Choosing per feature

    Ask the question from the CAP article with one addition: what does a wrong or stale answer cost, and do the records involved fit inside one transaction boundary?

    A per-feature decision map. The first question is the cost of a wrong answer; the second asks how far one transaction must reach, or how fresh the user’s own view must be.Notice Only the left branch needs the database to guard invariants across records. Most features in a typical product end up on the right.
    Feature Choose Why What you pay
    Payment ledger ACID in one database (shard by account if needed) A missing or duplicated entry is lost money; both lines of a double entry commit together A write ceiling per shard; cross-shard transfers become sagas
    Flash-sale inventory ACID for the reservation, BASE around it Overselling is refunds and angry customers; one atomic UPDATE … WHERE stock > 0 is the source of truth A hot row: split stock into several rows or put a queue in front (see design a flash sale)
    Likes and view counts BASE Off by a few for seconds is invisible; counting must never block the like button Approximate counts; a merge or reconciliation job
    Social feed BASE A post 5 seconds late is fine; fan-out must scale Feeds diverge briefly; deletes must chase every copy
    Shopping cart ACID inside the cart item or document, BASE across devices Each cart update is atomic in one record; price and stock are re-checked at checkout Merge rules when two devices edit; stale prices until checkout
    Search index BASE, fed by CDC or the outbox Seconds of lag are fine; re-read the primary when a user opens the item Index drift; periodic re-index or reconciliation
    ACID or BASE, feature by feature.

    Failure modes and war stories

    • The accidental distributed transaction. A workflow that calls three services synchronously and “rolls back” in a catch block has neither ACID’s atomicity nor a saga’s durable compensations; a crash halfway leaves partial state that nothing repairs. Make the steps explicit, persisted and resumable.
    • Compensations that race the forward step. A refund can reach the provider before the original capture has settled, and a cancel event can be processed before the create event. Compensations must be idempotent and handle “nothing to undo yet”, by waiting or by leaving a tombstone that the late forward step respects.
    • No isolation between sagas. One saga checks “1 seat left” in step 1 and reserves in step 3; another saga does the same in between, and both succeed. Put the check and the reservation in one atomic step, or use a semantic lock such as a HELD status that other sagas respect (see contention and double booking).
    • Transactions as a substitute for modelling. Port a normalised schema to DynamoDB or MongoDB, wrap every request in a multi-item transaction, and you get the costs of both worlds: double write capacity, the 100-item limit, and conflicts that cancel transactions under load. Model so most invariants live inside one item or document.
    • Reconciliation nobody reads. Mismatches pile up in a table no one opens, and months later the ledger and the provider disagree by lakhs of rupees. Every mismatch class needs an owner, an alert and a runbook.

    Cheat sheet

    Term Remember it as
    ACID The database guards invariants across rows: all-or-nothing, isolated, durable
    BASE Basically available, soft state, eventually consistent: the application repairs the gaps
    ACID’s C Invariants hold at every commit (declared constraints plus your code)
    BASE’s E Copies converge once writes stop; no promise about when
    2PC availability The product of participants: 99.9% × 99.9% ≈ 99.8%, about 43 extra minutes a month
    Convergence DynamoDB: 100 items, 4 MB, double writes; MongoDB 4.0 and 4.2; Cassandra LWT; Spanner, CockroachDB, YugabyteDB
    BASE toolkit Outbox, idempotency keys, idempotent consumers, sagas, commutative updates, conditional writes, reconciliation, visible pending states
    The design move A small ACID core around money and scarce stock; BASE edges for everything else
    What BASE costs Visible anomalies, repair code on every path, in-flight states to monitor

    How to say it in the interview

    “ACID and BASE are about who guards invariants. With ACID the database does: a transaction is all-or-nothing and isolated. With BASE I accept a short inconsistency window for availability and scale, and repair it with idempotent consumers, compensations and reconciliation. I choose per feature. Here, the order, payment record and ledger lines commit in one local transaction; restaurant, rider, notifications and search follow as outbox events, retried and idempotent, and a rejection runs a compensating refund behind a visible ‘waiting for restaurant’ status.”

    Isn’t BASE just NoSQL? Not any more. DynamoDB and MongoDB have multi-item transactions, Spanner and CockroachDB run serializable transactions across machines, and SQL systems behave BASE-style whenever they read from async replicas, sit behind caches or coordinate services.

    Why not use two-phase commit across the services? It makes every participant a hard dependency of every commit, so availability multiplies: two services at 99.9% give about 99.8%. Participants also hold locks while the coordinator decides, and a coordinator crash at the wrong moment leaves them blocked. Inside one distributed SQL database the engine manages this with consensus; across independent services, a saga is usually the better trade.

    How do you stop overselling in a BASE system? Keep the scarce-resource decision ACID: an atomic conditional decrement such as UPDATE stock SET qty = qty - 1 WHERE sku = ? AND qty > 0, in exactly one place. Payment, email and analytics follow as events. If one hot row limits throughput, split the stock across several rows.

    What if a compensation fails? Compensations must be idempotent and retried until they succeed, with an alert when they don’t. You can’t un-send an SMS, so order the saga so that steps without a natural undo run last.

    What does “eventually” mean in practice? Usually milliseconds to seconds of replication or consumer lag, but nothing bounds it. Measure it (consumer lag, outbox age), alert on it, and send reads that must be fresh to the source of truth.

    Where to go next

    References and further reading

    1. Towards Robust Distributed Systems (Brewer, PODC keynote, 2000)people.eecs.berkeley.eduThe original ACID-versus-BASE slide, “I think it’s a spectrum”, and Inktomi’s careful mixture of ACID and BASE subsystems.
    2. BASE: An Acid Alternative (Pritchett, ACM Queue, 2008)queue.acm.orgeBay’s functional partitioning, the 2PC availability arithmetic, persistent queues in the same database, idempotent and order-independent updates.
    3. Sagas (Garcia-Molina and Salem, SIGMOD 1987)cs.cornell.eduDefines a saga, semantic compensating transactions, and the loss of isolation between sagas.
    4. Amazon DynamoDB Developer Guide: Transactions, how it worksdocs.aws.amazon.comTransactWriteItems limits (100 items, 4 MB), isolation levels, the double capacity cost, client tokens, and ACID scoped to one Region in global tables.
    5. MongoDB Manual: Transactionsmongodb.comSingle-document atomicity, multi-document transactions on replica sets (4.0) and sharded clusters (4.2), and why they are no substitute for schema design.
    6. CockroachDB documentation: Transactionscockroachlabs.comDistributed ACID transactions, SERIALIZABLE by default, and client-side retries on 40001 errors.
    7. Spanner: TrueTime and external consistencycloud.google.comSerializable transactions with external consistency across servers and regions.
    8. YugabyteDB: Transaction isolation levelsdocs.yugabyte.comSerializable, snapshot and read committed isolation in a distributed SQL database.
    9. Designing robust and predictable APIs with idempotency (Stripe engineering blog)stripe.comIdempotency keys on mutating endpoints, and retries with exponential backoff and jitter.