Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Apa perbedaan ACID dan BASE pada database?
ACID berarti transaction bersifat atomic, consistent, isolated, dan durable: semua write di-commit bersamaan, aturan ditegakkan, pekerjaan bersamaan tidak saling mengganggu, dan commit selamat dari crash. BASE berarti basically available, soft state, eventually consistent: sistem tetap menjawab saat ada kegagalan dan replica bertemu di nilai yang sama belakangan. ACID menjaga kebenaran saat write, BASE menjaga ketersediaan dan menerima data basi sementara.
02Apakah BASE standar resmi atau hanya plesetan dari ACID?
BASE adalah plesetan. Dan Pritchett memperkenalkannya dalam artikel ACM Queue tahun 2008 sebagai lawan kimia dari acid, untuk menggambarkan gaya desain yang mengutamakan ketersediaan. Tidak ada spesifikasi formal di baliknya, jadi tidak ada database yang tersertifikasi BASE-compliant.
03Apakah PostgreSQL mendukung BASE atau hanya ACID?
Transaction PostgreSQL bersifat ACID di primary. Kamu tetap bisa mendapat perilaku mirip BASE dengan sengaja: read replica asynchronous bisa mengembalikan data sedikit lama, dan synchronous_commit off membuat crash bisa menghilangkan hingga sekitar 600 ms commit terbaru pada wal_writer_delay default, sementara database tetap konsisten.
04Kapan sebaiknya memakai eventual consistency, bukan transaction ACID?
Pakai untuk hal yang hasil basi atau gandanya sebentar saja murah dampaknya dan nilainya bisa dibangun ulang, seperti counter view, feed, cache, dan search index. Simpan uang, stok, dan catatan hukum di dalam transaction ACID. Putuskan per fitur dengan bertanya berapa biaya jawaban yang salah.
05Bisakah satu sistem memakai ACID dan BASE sekaligus?
Bisa, dan kebanyakan sistem produksi memang begitu. Desain yang umum meng-commit order, perubahan stok, dan satu baris outbox dalam satu transaction ACID, lalu worker mempublikasikan event ke consumer seperti dashboard dan notifikasi dengan pengiriman at-least-once. Bahkan DynamoDB, yang biasanya digolongkan sebagai store bergaya BASE, menyediakan transaction all-or-nothing hingga 100 aksi write.
ACID adalah empat jaminan dari transaksi database: write yang atomic, consistent, isolated, dan durable, sehingga uang berpindah utuh atau tidak sama sekali dan selamat dari crash. BASE, yaitu basically available, soft state, eventually consistent, adalah label informal, bukan standar, untuk desain yang menukar jaminan itu demi ketersediaan. Pakai ACID untuk uang dan stok, BASE untuk counter dan feed.
Dua statement UPDATE dan satu crash di antaranya sudah cukup untuk membuat 200000 rupiah hilang dari ledger. Database tidak protes, karena masing-masing statement valid kalau berdiri sendiri. Celah kecil itulah alasan ACID ada, dan cara terbaik untuk memahami apa yang dilepas oleh BASE.
Post ini membahas tiap huruf ACID lewat kegagalan PostgreSQL yang bisa kamu reproduksi, memperlakukan BASE secara jujur sebagai kosakata desain dan bukan spesifikasi, lalu ditutup dengan checklist per fitur. Setiap klaim perilaku PostgreSQL dan DynamoDB berasal dari dokumentasi resminya, dan setiap angka di bawah adalah aritmetika yang bisa kamu hitung sendiri.
Apa yang dijamin ACID, dan apa yang rusak tanpa transaction?
Atomicity adalah huruf yang paling mudah dilihat gagal. Sari punya 500000 dan Budi punya 100000, jadi total uang di sistem 600000. Transfer 200000 adalah dua statement UPDATE, dan script di bawah menjalankannya dua kali: sekali sebagai statement biasa, sekali di dalam BEGIN dan COMMIT. PostgreSQL memperlakukan setiap statement di luar transaction sebagai transaction implisitnya sendiri, jadi pada versi pertama debit langsung di-commit sendirian.
CREATE TABLE accounts (
id int PRIMARY KEY,
owner text NOT NULL,
balance numeric(12,2) NOT NULL CHECK (balance >= 0)
);
INSERT INTO accounts VALUES (1, 'Sari', 500000), (2, 'Budi', 100000);
-- Total money in the system: 500000 + 100000 = 600000
-- Wrong: two autocommit statements. Each one is its own transaction.
UPDATE accounts SET balance = balance - 200000 WHERE id = 1; -- committed
-- ...the connection drops or the app crashes right here...
UPDATE accounts SET balance = balance + 200000 WHERE id = 2; -- never runs
-- Sari 300000 + Budi 100000 = 400000. 200000 has vanished.
-- Right: one transaction. Both updates commit together or neither does.
BEGIN;
UPDATE accounts SET balance = balance - 200000 WHERE id = 1;
UPDATE accounts SET balance = balance + 200000 WHERE id = 2;
COMMIT;
-- A crash before COMMIT leaves Sari 500000 + Budi 100000 = 600000.
Pada versi yang salah, crash di antara kedua statement meninggalkan 300000 ditambah 100000, yaitu 400000, dan tidak ada yang bisa menjelaskan ke mana 200000 pergi. Pada versi yang benar, crash yang sama meninggalkan 600000, karena transaction yang tidak pernah di-commit dianggap tidak pernah terjadi. Tutorial PostgreSQL menyatakan syaratnya langsung: kalau ada yang salah di tengah jalan, tidak ada langkah yang sudah dijalankan yang boleh berlaku.
Driver yang mengirim tiap query sendiri-sendiri memberi kamu satu transaction per query. Dua pemanggilan berurutan ke connection pool bahkan bisa berjalan di dua koneksi berbeda. Pastikan service layer kamu benar-benar membuka BEGIN, menjalankan kedua update di koneksi yang sama, dan memanggil ROLLBACK di jalur error.
Apakah C di ACID sama dengan consistency di CAP atau BASE?
Tidak, dan salah kaprah ini menjadi sumber separuh debat ACID lawan BASE yang keliru. Di ACID, consistency berarti sebuah transaction membawa database dari satu state yang memenuhi aturannya ke state lain yang juga memenuhinya. Aturannya adalah constraint yang kamu deklarasikan: NOT NULL, CHECK, UNIQUE, FOREIGN KEY. Pada contoh, CHECK (balance >= 0) menolak saldo minus, dan pelanggarannya membatalkan seluruh transaction, termasuk kredit yang belum sempat berjalan.
BEGIN;
UPDATE accounts SET balance = balance - 700000 WHERE id = 1;
-- ERROR: new row for relation "accounts" violates check constraint
-- "accounts_balance_check"
-- The transaction is now aborted: every later statement is refused
-- until you ROLLBACK, so a half-applied transfer cannot be committed.
ROLLBACK;
-- What the database will NOT check for you: any rule you never declared.
-- "A user may not transfer to their own account" is not enforced by this
-- schema. Declare it (CHECK, FOREIGN KEY, UNIQUE) or enforce it in code.
Akibatnya, database hanya menegakkan invariant yang sudah kamu tulis. Saya lebih suka menaruh aturan seperti stok tidak boleh negatif di schema, bukan di kode service, karena constraint berlaku untuk semua penulis, termasuk script migrasi yang dijalankan seseorang larut malam. Consistency dalam arti kesepakatan antar replica adalah pertanyaan lain, dan post pendamping tentang eventual consistency explained membahasnya.
Apa yang salah tanpa isolation, dan bagaimana Postgres mencegahnya?
Isolation menyangkut transaction yang berjalan bersamaan dan saling melihat pekerjaan yang belum selesai. Level default PostgreSQL adalah Read Committed, dan menurut dokumentasinya dua SELECT berurutan dalam satu transaction bisa melihat data berbeda kalau transaction lain melakukan commit di antaranya. Itu sebabnya pola read-then-write pada kode di atas menghilangkan uang: kedua kasir membaca 500000, keduanya menulis 400000, dan saldo akhir 100000 lebih tinggi dari yang benar, yaitu 500000 dikurangi 100000 dikurangi 100000 sama dengan 300000.
-- Sari's balance is 500000. Two cashiers each take 100000 at the same time.
-- Expected result: 500000 - 100000 - 100000 = 300000.
-- Wrong: read in the application, compute there, write the result back.
-- Session A: SELECT balance FROM accounts WHERE id = 1; -- 500000
-- Session B: SELECT balance FROM accounts WHERE id = 1; -- 500000
-- Session A: UPDATE accounts SET balance = 400000 WHERE id = 1; COMMIT;
-- Session B: UPDATE accounts SET balance = 400000 WHERE id = 1; COMMIT;
-- Final balance: 400000. One withdrawal was silently lost.
-- Right: let the database do the arithmetic on the current row.
UPDATE accounts SET balance = balance - 100000 WHERE id = 1;
-- Under Read Committed the second session waits for the first to commit,
-- then re-reads the row and subtracts from 400000. Final balance: 300000.
-- Or, when you must read first and then decide, lock the row you read:
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- ...application logic...
UPDATE accounts SET balance = 300000 WHERE id = 1;
COMMIT;
Solusinya biasanya statement yang lebih baik, bukan isolation level yang lebih tinggi. Aritmetika di dalam UPDATE, atau row lock dengan SELECT FOR UPDATE, menutup celah yang satu ini. Kalau kamu menaikkan level ke Repeatable Read atau Serializable, dokumentasi menyebut aplikasi harus siap mengulang seluruh transaction setelah serialization failure, yang dilaporkan dengan SQLSTATE 40001. Apa yang diizinkan tiap level dibahas di post database transaction isolation levels explained, jadi tabelnya tidak saya ulang di sini.
Cari pola ini di codebase kamu: select saldo, hitung di aplikasi, lalu update dengan angka literal. Setiap temuan adalah lost update yang menunggu dua request bersamaan. Ganti dengan SQL relatif seperti balance = balance - 100000, dengan CHECK constraint di belakangnya.
Apa yang dijanjikan durability, dan kapan diam-diam melemah?
Durability berarti transaction yang sudah di-commit selamat dari crash. Dokumentasi PostgreSQL menjelaskan mekanismenya: semua update sebuah transaction dicatat di penyimpanan permanen sebelum transaction dilaporkan selesai. Dengan setelan default synchronous_commit on, COMMIT menunggu flush write-ahead log lokal ke disk sebelum mengembalikan sukses.
SHOW synchronous_commit; -- on (the default): COMMIT waits for the WAL flush
SHOW wal_writer_delay; -- 200ms by default
-- Relax durability for ONE low-value transaction, not the whole server.
-- Worst case: this write is lost in a crash, up to 3 x 200ms = 600ms later.
-- The database stays consistent; the write behaves as if it had aborted.
BEGIN;
SET LOCAL synchronous_commit = off;
INSERT INTO page_views (path, viewed_at) VALUES ('/blog/acid-vs-base', now());
COMMIT;
-- Never do this on a database you cannot rebuild from elsewhere:
-- fsync = off (a crash can cause unrecoverable data corruption)
Setelan itu bisa dilonggarkan per transaction, yang merupakan sedikit cara berpikir BASE di dalam database ACID. Dengan synchronous_commit off, dokumentasi menyebut crash bisa menghilangkan commit yang baru dilaporkan hingga tiga kali wal_writer_delay, yaitu 3 x 200 ms = 600 ms pada nilai default, namun database tetap konsisten, seolah transaction itu di-abort. Mematikan fsync lain lagi: dokumentasi memperingatkan data corruption yang tidak bisa dipulihkan setelah crash. Pakai yang pertama untuk log page view, dan jangan pernah yang kedua untuk data yang tidak bisa kamu bangun ulang.
Apa itu BASE, dan apakah itu standar resmi?
BASE adalah singkatan dari Basically Available, Soft state, Eventually consistent. Dan Pritchett memperkenalkannya dalam artikel ACM Queue tahun 2008 berjudul BASE: An Acid Alternative, dan namanya adalah plesetan kimia: base atau basa adalah lawan dari acid atau asam. Ia menggambarkan gaya desain, bukan spesifikasi. Saya tidak tahu ada badan standar yang mendefinisikannya, dan tidak ada yang mensertifikasi database sebagai BASE-compliant seperti sebuah engine bisa dinilai dari perilaku ACID-nya.
Ketiga hurufnya longgar. Basically available berarti sistem tetap menjawab saat sebagian gagal, mungkin dengan data basi. Soft state berarti data bisa berubah seiring waktu tanpa input baru, karena replica masih bergerak menuju kesepakatan. Eventually consistent berarti kalau write berhenti, semua replica akhirnya memiliki nilai yang sama. Post eventual consistency explained membahas cara konvergensi itu bekerja; di sini intinya adalah trade-off-nya.
Pertanyaan
ACID
BASE
Apa yang dijanjikan?
Transaction utuh atau tidak sama sekali, dan setelah commit bersifat permanen
Sistem tetap menjawab, dan replica bertemu di nilai yang sama setelah write berhenti
Saat terjadi kegagalan
Melakukan rollback atau menolak write
Menerima write dan bisa menyajikan data basi
Membaca tepat setelah write
Melihat nilai yang sudah di-commit di primary
Bisa melihat nilai lama untuk sementara
Di mana biayanya jatuh
Lock, pengecekan konflik, dan retry saat write
Rekonsiliasi dan penanganan duplikat belakangan
Kecocokan umum
Saldo, stok, order, invoice
Counter, feed, cache, search index
Status formal
Properti bernama yang ditegakkan engine
Label dari artikel 2008, tanpa spesifikasi
Baca tabel ini sebagai dua ujung dari satu tuas, bukan dua kubu. Kolom kanan menggambarkan niat, dan satu produk bisa berada di kolom mana pun tergantung operasi yang kamu panggil.
Sistem nyata mana yang mencampur ACID dan BASE?
Hampir semua stack produksi melakukannya, sering kali di dalam satu database. Tiga bentuk yang umum:
PostgreSQL dengan replica: primary memberi write ACID penuh, sementara read replica asynchronous bisa tertinggal, jadi pembacaan di sana bisa mengembalikan nilai lama. Dengan synchronous standby, dokumentasi menyebut commit menunggu sampai standby menerima dan mem-flush commit record, sehingga durability antar server naik dengan harga latency.
Amazon DynamoDB: biasanya digolongkan sebagai store bergaya BASE, namun TransactWriteItems mengelompokkan hingga 100 aksi write, maksimal 4 MB total, menjadi satu operasi all-or-nothing. Dokumentasi menambahkan bahwa jaminan ACID hanya berlaku di Region tempat write dilakukan, dan eventually consistent read tepat setelah transaction masih bisa mengembalikan state lama.
Cache di depan database, seperti Redis di samping Postgres pada satu server: cache adalah soft state sejak desainnya. Ia boleh basi dan bisa dibangun ulang, sementara Postgres tetap menjadi source of truth.
Jadi sebuah store jarang berstatus ACID atau BASE secara keseluruhan. Pertanyaan yang berguna adalah operasi mana yang kamu panggil dan apa yang dijanjikan operasi itu.
Bagaimana memilih ACID atau BASE untuk tiap fitur?
Pilih per fitur, berdasarkan biaya jawaban yang salah. Ajukan empat pertanyaan ini berurutan untuk setiap jalur write:
Apakah write yang hilang atau salah merugikan uang, stok, atau catatan hukum? Kalau ya, tempatnya di dalam transaction ACID.
Apakah user harus langsung melihat write miliknya sendiri? Kalau ya, baca dari primary atau pakai strongly consistent read, karena replica yang tertinggal tidak cukup.
Apakah nilainya bisa dibangun ulang dari sumber lain, misalnya counter yang dihitung ulang dari order? Kalau ya, ia boleh tinggal di edge BASE.
Apakah consumer aman melihat event yang sama dua kali, atau sedikit terlambat? Kalau ya, pengiriman at-least-once cukup. Kalau tidak, buat operasinya idempotent atau tetap di dalam transaction.
Point of sale carwash menunjukkan pemisahannya. Baris pembayaran dan pengurangan stok produk wax adalah inti. Counter dashboard cuci hari ini dan pesan struk ke pelanggan adalah edge. Ini contoh hitungan, bukan hasil pengukuran.
Bagaimana mendesain ACID core dengan BASE edge?
Simpan kebenaran yang memindahkan uang di satu store transaksional, dan biarkan hal lain bergantung padanya lewat event. Kode di bawah menulis order, pengurangan stok, dan satu baris outbox dalam satu transaction, lalu worker terpisah mempublikasikannya sesudahnya. Karena baris event di-commit secara atomic bersama datanya, tidak mungkin ada order tanpa event atau event tanpa order.
-- ACID core: the order, the stock decrement and the outbox row commit together.
BEGIN;
INSERT INTO orders (id, customer_id, total) VALUES (1042, 7, 85000);
UPDATE stock SET qty = qty - 1 WHERE sku = 'WAX-01' AND qty > 0;
-- 0 rows updated means sold out: ROLLBACK here, nothing leaks out.
INSERT INTO outbox (aggregate_id, topic, payload)
VALUES (1042, 'order.created', '{"orderId":1042}');
COMMIT;
-- BASE edge: a separate worker drains the outbox. Delivery is at-least-once,
-- so every consumer (receipt message, dashboard counter, search index) must
-- tolerate seeing the same event twice.
SELECT id, topic, payload FROM outbox
WHERE sent_at IS NULL ORDER BY id LIMIT 50
FOR UPDATE SKIP LOCKED;
-- publish each row, then: UPDATE outbox SET sent_at = now() WHERE id = ANY(...)
Edge membayar dengan pengiriman at-least-once, jadi consumer harus idempotent, dan dashboard tertinggal selama worker membutuhkan waktu. Terima keterlambatan itu secara eksplisit. Post transactional outbox membahas Postgres lebih dalam, dan post saga membahas apa yang dilakukan ketika core itu sendiri melintasi beberapa service.
ACID dan BASE bukan dua kubu yang bersaing, melainkan dua janji yang bisa kamu buat per write. Bungkus semua yang memindahkan uang atau stok dalam transaction, deklarasikan invariant sebagai constraint, pilih update relatif daripada read-modify-write, dan biarkan counter, feed, dan cache menyusul lewat outbox. Kalau ada yang bertanya database kamu yang mana, tanyakan balik operasi yang mana.