Data Consistency Patterns — System Design

Dalam sistem terdistribusi, memastikan konsistensi data antar node dan service adalah salah satu tantangan terbesar. Level Konsistensi: Strong Consistency →…

Consistency di distributed system adalah janji tentang data mana yang boleh dibaca, saat datanya punya beberapa salinan di beberapa server. Bayangin kampus dengan papan pengumuman di tiga gedung. Bagian akademik menempel pengumuman baru di gedung pusat, lalu kurir membawa fotokopinya ke gedung lain. Kurir ke gedung A cepat, kurir ke gedung B lambat. Mahasiswa di gedung B masih membaca jadwal lama, padahal di pusat sudah ganti. Papan di gedung pusat itu primary, papan lain replica, dan kurirnya replikasi. Model consistency menjawab: siapa boleh membaca papan yang mana, dan kapan.

🧪 Tebakan cepat: Budi mengganti email akunnya dari [email protected] ke [email protected], lalu me-reload halaman profil 500 ms kemudian. Read-nya dilayani replica yang tertinggal 1.000 ms. Email mana yang Budi lihat? Tebak dulu, jawabannya ada di bagian "Replica yang tertinggal".

Replica yang tertinggal

Di lesson Database Scaling, TiketNada menambah read replica: salinan database PostgreSQL yang melayani SELECT, sementara semua write tetap ke primary. Replikasinya asynchronous: primary menjawab "sukses" begitu write-nya tersimpan di primary, lalu mengirim perubahannya ke replica belakangan. Jarak waktu sampai replica ikut punya data itu disebut replication lag. Saat war tiket replica sibuk, jadi lag-nya membengkak. Model contoh lesson ini: replica tertinggal 1.000 ms.

ms Primary Replica Budi reload
0 UPDATE: email jadi [email protected] masih [email protected]
500 [email protected] masih [email protected] baca dari replica: [email protected]
1.000 [email protected] menerapkan perubahan: [email protected]

Jawaban tebakannya: Budi melihat email lamanya sendiri. Dia mengira perubahannya gagal, lalu mengganti lagi, atau menghubungi CS. Data-nya gak rusak; 500 ms lagi replica akan menyusul. Masalahnya ada di janji yang diberikan sistem ke pembacanya.

Strong dan eventual

Dua ujung janjinya:

Kebanyakan data TiketNada aman dengan eventual: sisa kursi di halaman konser telat 1 detik gak merugikan siapa pun. Tapi ada dua kasus yang bikin pengguna bingung, dan dua janji di tengah-tengah untuk mengatasinya.

Read-your-writes

Read-your-writes menjanjikan satu hal: orang yang menulis selalu melihat tulisannya sendiri. Orang lain boleh masih melihat data lama sebentar. Ini cukup untuk Budi, dan gak memaksa semua read ke primary.

Caranya di PostgreSQL: setelah write, aplikasi mencatat posisi log-nya (LSN, Log Sequence Number) di session Budi, dari pg_current_wal_lsn(). Sebelum membaca dari replica, cek apakah replica sudah menerapkan posisi itu:

// read-your-writes: yang baru menulis membaca dari primary,
// sampai replica sudah menerapkan tulisannya
async function sumberBaca(sesi) {
  if (!sesi.lsnTerakhir) return replica; // belum pernah menulis
  const { rows } = await replica.query(
    "SELECT pg_last_wal_replay_lsn() >= $1::pg_lsn AS sudah",
    [sesi.lsnTerakhir]
  );
  return rows[0].sudah ? replica : primary;
}

Cara yang lebih sederhana: setelah menulis, baca dari primary selama beberapa detik (Rails memakai 2 detik secara default untuk ini).

Causal consistency

Kasus kedua butuh dua penulis. Di halaman Konser Nusantara ada diskusi. Database diskusi TiketNada di-shard per penulis (lesson Database Scaling), jadi:

Siti membaca di Pertanyaan Budi (shard-1, tiba ms 1.200) Jawaban admin (shard-2, tiba ms 700)
ms 800, eventual belum ada "Ada, di basement P2"
ms 800, causal belum ada ditahan
ms 1.200, causal ada ada

Dengan eventual, Siti melihat jawaban untuk pertanyaan yang belum ada. Causal consistency mencegahnya: kalau tulisan B dibuat setelah penulisnya melihat tulisan A (B bergantung pada A), siapa pun yang melihat B juga harus melihat A. Caranya, jawaban admin membawa catatan "aku membalas pertanyaan versi sekian di shard-1", dan jawaban baru ditampilkan setelah pertanyaannya terlihat. Read-your-writes gak menolong Siti di sini, karena Siti sendiri gak menulis apa-apa. Sebaliknya, causal sudah mencakup read-your-writes: read Budi bergantung pada tulisannya sendiri.

Janji Yang dijamin Harga
Strong semua read melihat write terbaru semua read ke primary, atau write menunggu replica
Causal yang bergantung gak muncul sebelum asalnya mencatat dan memeriksa ketergantungan
Read-your-writes penulis melihat tulisannya sendiri read penulis kadang ke primary
Eventual akhirnya sama data lama bisa terbaca sebentar

Outbox: database dan event gak boleh beda cerita

Masalah consistency yang lain: satu aksi mengubah dua sistem. Saat pesanan #1001 dibayar, order mengubah status di PostgreSQL dan mengirim event order.dibayar ke queue e-tiket (lesson Message Queue). Kalau proses order crash di antara keduanya:

Outbox pattern menyelesaikannya: event ditulis sebagai baris di tabel outbox, di transaksi database yang sama dengan perubahan datanya. Keduanya tersimpan atau keduanya batal. Proses terpisah (relay) membaca tabel itu dan mem-publish ke queue.

// order: tandai #1001 dibayar DAN catat event-nya, dalam SATU transaksi
const client = await pool.connect();
try {
  await client.query("BEGIN");
  await client.query("UPDATE orders SET status = 'dibayar' WHERE id = $1", [1001]);
  await client.query(
    "INSERT INTO outbox (topic, payload) VALUES ($1, $2)",
    ["order.dibayar", JSON.stringify({ pesanan: 1001 })]
  );
  await client.query("COMMIT");
} catch (err) {
  await client.query("ROLLBACK");
  throw err;
} finally {
  client.release();
}

// relay: proses terpisah, jalan tiap 1 detik
async function kirimOutbox() {
  const { rows } = await pool.query(
    "SELECT id, topic, payload FROM outbox WHERE sent_at IS NULL ORDER BY id LIMIT 100"
  );
  for (const row of rows) {
    await queue.publish(row.topic, row.payload);
    await pool.query("UPDATE outbox SET sent_at = now() WHERE id = $1", [row.id]);
  }
}
setInterval(kirimOutbox, 1000);

Daripada polling tiap detik, relay juga bisa membaca log PostgreSQL langsung (CDC, Change Data Capture, misalnya dengan Debezium), jadi event terkirim lebih cepat.

Konsistensi lintas service: sekilas Saga

Outbox menjaga satu service tetap jujur. Kalau satu aksi butuh perubahan di beberapa service (pesan kursi di tiket, potong kartu di bayar), dipakai Saga: urutan transaksi lokal di tiap service, dan kalau satu langkah gagal, langkah yang sudah jalan dibatalkan dengan compensating transaction (misalnya kursi dilepas lagi). Kenapa gak satu transaksi besar saja dibahas di lesson Distributed Transactions, dan Saga dibedah di lesson Saga Pattern.

⚠️ Jebakan yang sering ketemu

TL;DR: Saat data punya beberapa salinan, model consistency menentukan apa yang boleh terbaca. Replikasi asynchronous membuat replica tertinggal (replication lag, di model Lab 1.000 ms), jadi Budi bisa melihat email lamanya sendiri setelah mengganti. Strong consistency: setiap read mengembalikan data terbaru, mahal karena butuh koordinasi. Eventual consistency: data akhirnya sama, tapi sementara bisa stale, murah dan scalable. Read-your-writes: penulis selalu melihat tulisannya sendiri (cek LSN replica, atau baca dari primary sebentar). Causal: tulisan yang bergantung gak boleh muncul sebelum asalnya (jawaban sebelum pertanyaan). Outbox pattern: simpan event di tabel outbox dalam transaksi yang sama dengan datanya, lalu relay (polling atau CDC) mem-publish ke message broker; event terkirim paling sedikit sekali, jadi consumer harus idempotent. Saga: urutan transaksi lokal per service dengan compensating transaction kalau ada langkah yang gagal.

Yang akan kamu pelajari