Debug backend: salah cache key pada data sensitif biasanya terlihat seperti bug tampilan biasa, padahal dampaknya bisa jauh lebih serius: kebocoran konteks, salah atribusi, risiko reputasi, dan pelanggaran privasi. Dalam studi kasus ini, endpoint profil publik menampilkan ringkasan donasi atau afiliasi politik yang seharusnya berbeda per profil, tetapi hasil cache tertukar karena key cache tidak memasukkan konteks yang tepat.

Kasus seperti ini relevan ketika aplikasi memuat data publik yang tetap sensitif secara sosial, misalnya relasi figur publik dengan donor, organisasi, atau partai. Konteks editorial seperti diskusi publik di media sosial—misalnya tautan referensi ini—cukup untuk menunjukkan risiko reputasi, tetapi fokus artikel ini tetap pada engineering problem: bagaimana bug terjadi, bagaimana menemukannya, dan bagaimana mencegahnya terulang.

Gejala Awal: Data Profil A Menampilkan Donasi Milik Profil B

Insiden biasanya tidak langsung terlihat sebagai kebocoran cache. Gejalanya sering tampak acak:

  • Pengguna membuka profil publik tokoh-A, tetapi blok “donasi terkait” menampilkan data yang sebelumnya terlihat di tokoh-B.
  • Refresh halaman kadang memperbaiki tampilan, kadang tidak.
  • Bug lebih sering terjadi saat trafik tinggi atau setelah deploy yang meningkatkan hit ratio cache.
  • Log aplikasi tidak menunjukkan error 500, karena data tetap valid secara struktur.

Ini yang membuat bug berbahaya: sistem tidak crash, metrik error rendah, tetapi integritas data rusak.

Timeline Insiden yang Realistis

1. Laporan awal dari tim konten atau support

Tim non-engineering biasanya pertama kali melihat ada profil yang menampilkan relasi donasi yang tidak cocok. Karena data tampak “masuk akal”, laporan awal sering berbunyi: “sepertinya profilnya ketuker” alih-alih “ada kebocoran cache”.

2. Verifikasi manual

Engineer mencoba membuka dua profil berbeda secara bergantian, sering dari browser incognito, lalu membandingkan respons JSON atau HTML hasil render server-side. Pada tahap ini, penting memisahkan kemungkinan bug di frontend dari bug di backend/API.

3. Dugaan awal yang keliru

Dugaan yang sering muncul:

  • JOIN database salah.
  • ORM relation menggunakan eager loading yang salah.
  • CDN men-cache HTML terlalu agresif.
  • Frontend state bocor antar route.

Semua ini valid untuk dicek, tetapi pada kasus ini akar masalah ada di backend cache layer.

4. Konfirmasi lewat tracing dan inspeksi cache

Setelah request tracing diaktifkan, terlihat bahwa dua request untuk profil berbeda membaca entry cache yang sama. Di situlah masalah menjadi jelas: cache key terlalu generik.

Reproduksi Bug Secara Konsisten

Bug cache sulit diperbaiki kalau tidak bisa direproduksi. Buat langkah reproduksi yang deterministik:

  1. Siapkan dua profil publik berbeda, misalnya public_id=A dan public_id=B.
  2. Pastikan masing-masing punya data donasi atau afiliasi yang berbeda dan mudah dibedakan.
  3. Hapus cache terkait.
  4. Panggil endpoint profil A terlebih dahulu.
  5. Panggil endpoint profil B.
  6. Bandingkan payload bagian data sensitif.

Jika key cache hanya berbasis route umum seperti profile:summary, maka request kedua bisa menerima hasil request pertama.

# contoh sederhana dengan curl
curl -s https://api.example.com/public/profile/A | jq '.donation_summary'
curl -s https://api.example.com/public/profile/B | jq '.donation_summary'

Kalau hasil untuk B identik dengan A padahal database berbeda, ada indikasi kuat key cache tidak menyertakan identitas profil atau konteks lain yang diperlukan.

Log dan Trace yang Paling Membantu

Untuk kasus debug backend: salah cache key, log biasa seperti status code belum cukup. Yang berguna justru metadata cache dan konteks request.

Tambahkan field log berikut

  • request_id atau trace ID
  • route
  • public_profile_id atau slug profil
  • tenant_id jika sistem multi-tenant
  • viewer_context bila hasil berbeda untuk anonymous vs authenticated
  • cache_key
  • cache_hit / cache_miss
  • cache_ttl_remaining bila tersedia

Contoh log yang mengarah ke akar masalah

request_id=abc123 route=/public/profile/A profile_id=A cache_key=profile:summary hit=false
request_id=def456 route=/public/profile/B profile_id=B cache_key=profile:summary hit=true

Dua profil berbeda menggunakan cache_key yang sama. Itu red flag paling jelas.

Gunakan distributed tracing bila ada

Jika memakai OpenTelemetry atau tracing sejenis, tambahkan span attribute untuk:

  • nama cache operation
  • key atau hash key
  • hasil hit/miss
  • dependency call ke database

Pola umum yang terlihat: request pertama memukul database lalu menulis cache; request kedua melewati database dan langsung membaca cache yang salah.

Root Cause: Cache Key Terlalu Generik, TTL Panjang, dan Invalidasi Lemah

1. Cache key tidak memasukkan konteks data

Ini akar masalah utama. Endpoint profil publik kadang dianggap homogen karena route-nya sama, padahal data di dalamnya bergantung pada:

  • ID profil atau slug
  • tenant atau organisasi
  • lokalitas/bahasa, jika memengaruhi isi
  • hak akses viewer, jika sebagian field disamarkan
  • flag eksperimen atau versi serialisasi, jika struktur output bisa berbeda

Kalau salah satu konteks ini diabaikan, data bisa saling tertukar.

2. TTL terlalu panjang untuk data sensitif

TTL panjang memperparah dampak. Misalnya, key yang salah tersimpan selama beberapa menit atau jam. Walaupun bug awal hanya dipicu sekali, akibatnya menyebar ke banyak request berikutnya.

3. Invalidasi tidak terhubung ke perubahan sumber data

Ketika data donasi diperbarui, cache lama seharusnya dibuang. Jika invalidasi hanya berbasis waktu dan tidak berbasis event pembaruan, maka entry yang salah atau usang bertahan lebih lama dari yang seharusnya.

4. Endpoint publik sering dianggap aman untuk di-cache secara agresif

Ini asumsi yang sering keliru. “Publik” tidak sama dengan “tidak sensitif”. Data publik tetap bisa sensitif secara reputasi, kontekstual, atau legal.

Pseudocode Sebelum: Implementasi yang Rentan

Contoh di bawah menunjukkan pola bug yang umum. Route benar, query benar, tetapi key cache salah karena terlalu umum.

function getPublicProfile(profileId, viewerContext) {
  const cacheKey = "profile:summary";

  return cache.remember(cacheKey, 900, () => {
    const profile = db.profiles.findPublicById(profileId);
    const donationSummary = db.donations.getPublicSummaryForProfile(profileId);

    return {
      profile: serializeProfile(profile),
      donation_summary: serializeDonationSummary(donationSummary, viewerContext)
    };
  });
}

Masalahnya:

  • profileId tidak masuk ke key.
  • viewerContext mungkin memengaruhi serialisasi, tetapi tidak masuk ke key.
  • TTL 900 detik cukup lama untuk menyebarkan data salah.

Pseudocode Sesudah: Key Berbasis Konteks dan Versi

Perbaikannya bukan sekadar menambahkan ID profil. Key harus dibangun dari semua dimensi yang memengaruhi output.

function getPublicProfile(profileId, viewerContext, tenantId, locale) {
  const viewerScope = viewerContext.isAuthenticated ? "auth" : "anon";
  const schemaVersion = "v2";

  const cacheKey = [
    "public-profile",
    schemaVersion,
    `tenant:${tenantId}`,
    `profile:${profileId}`,
    `viewer:${viewerScope}`,
    `locale:${locale}`
  ].join(":");

  return cache.remember(cacheKey, 120, () => {
    const profile = db.profiles.findPublicById(profileId, tenantId);
    const donationSummary = db.donations.getPublicSummaryForProfile(profileId, tenantId);

    return {
      profile: serializeProfile(profile, locale),
      donation_summary: serializeDonationSummary(donationSummary, viewerContext)
    };
  });
}

Kenapa pendekatan ini bekerja:

  • Key unik per konteks, sehingga output untuk profil A tidak tertimpa atau dipakai ulang oleh profil B.
  • Schema/version segment memudahkan rollout perubahan serialisasi tanpa bentrok dengan cache lama.
  • TTL lebih pendek membatasi blast radius jika ada bug yang lolos.

Perbaikan Lanjutan: Invalidasi dan Desain Cache yang Lebih Aman

Gunakan invalidasi berbasis event

Saat data profil, relasi afiliasi, atau ringkasan donasi berubah, kirim event domain untuk menghapus atau memutar key terkait.

function onProfileDonationDataChanged(profileId, tenantId) {
  cache.invalidateByPrefix(`public-profile:v2:tenant:${tenantId}:profile:${profileId}:`);
}

Implementasi aktual bergantung pada storage cache. Tidak semua backend cache mendukung invalidasi prefix secara efisien. Jika tidak didukung, simpan indeks key atau gunakan strategi versioning per entitas.

Pertimbangkan entity versioning

Alih-alih mencari semua key, Anda bisa menambahkan versi per profil:

const entityVersion = db.profileCacheVersion.get(profileId) || 1;
const cacheKey = `public-profile:v2:tenant:${tenantId}:profile:${profileId}:ver:${entityVersion}:viewer:${viewerScope}:locale:${locale}`;

Saat ada perubahan data, cukup naikkan versi. Semua request baru otomatis memakai key baru tanpa perlu menyapu cache lama satu per satu.

Jangan cache field sensitif jika manfaatnya kecil

Jika blok data sangat sensitif dan query-nya tidak terlalu mahal, salah satu pilihan paling aman adalah tidak men-cache blok tersebut, atau men-cache bagian yang benar-benar stabil saja. Trade-off-nya adalah beban database lebih tinggi, tetapi risiko salah tampil lebih rendah.

Strategi Test Regresi yang Wajib Ditambahkan

Setelah bug ditemukan, perbaikan tanpa test regresi hampir pasti akan kambuh dalam bentuk lain.

1. Test isolasi antar profil

test("cache profil publik tidak tertukar antar profileId", () => {
  seedProfile("A", { donationSummary: "Donor A" });
  seedProfile("B", { donationSummary: "Donor B" });

  const resA = getPublicProfile("A", anonContext, "tenant-1", "id");
  const resB = getPublicProfile("B", anonContext, "tenant-1", "id");

  expect(resA.donation_summary).toContain("Donor A");
  expect(resB.donation_summary).toContain("Donor B");
});

2. Test isolasi antar tenant

Wajib jika slug atau ID profil bisa bentrok antar tenant, atau query source dibatasi tenant.

3. Test perbedaan viewer context

Jika anonymous dan authenticated menerima payload berbeda, test bahwa cache tidak dibagi secara salah.

4. Test invalidasi

Setelah pembaruan data, request berikutnya harus mengembalikan isi baru, bukan isi dari cache lama.

5. Test konkruensi ringan

Buat dua request hampir bersamaan untuk profil berbeda dan pastikan tidak ada kontaminasi data. Ini membantu menangkap pola race condition yang kadang tersembunyi di wrapper cache custom.

Metrik Observabilitas yang Perlu Dipantau

Hit ratio tinggi tidak selalu berarti sehat. Untuk endpoint sensitif, tambahkan metrik yang lebih bermakna.

  • Cache hit/miss per endpoint dan per key family
  • Jumlah invalidasi per entitas
  • Perbedaan cardinality key: kalau endpoint ribuan profil hanya menghasilkan segelintir key, ada indikasi key terlalu generik
  • Mismatch detector: sampling respons dan cocokkan fingerprint profil dengan fingerprint payload donasi
  • Staleness age: umur data yang disajikan dari cache

Teknik yang praktis adalah menyimpan fingerprint ringan di payload internal, misalnya source_profile_id atau hash dari query utama, lalu memverifikasi dalam logging bahwa profil yang diminta sama dengan profil sumber data.

Catatan: jangan kirim metadata internal sensitif ke klien publik. Simpan untuk logging, tracing, atau header debug yang hanya aktif di environment terbatas.

Kesalahan Umum Saat Memperbaiki Bug Ini

  • Hanya menambahkan profileId ke key, padahal tenant atau viewer context juga memengaruhi output.
  • Menghapus cache sekali saat insiden, tetapi tidak memperbaiki desain key.
  • TTL dipersingkat, lalu dianggap cukup. TTL pendek mengurangi dampak, bukan menghilangkan akar masalah.
  • Mengandalkan nama route sebagai key utama.
  • Tidak menambahkan test regresi karena bug dianggap “sekali saja”.
  • Tidak mengaudit layer cache lain seperti reverse proxy, CDN, atau fragment cache.

Checklist Pencegahan untuk Endpoint Profil Publik

  • Identifikasi semua dimensi yang memengaruhi output: entitas, tenant, viewer, locale, versi serialisasi.
  • Bangun cache key dari dimensi tersebut secara eksplisit.
  • Gunakan namespace atau version prefix agar rollout perubahan aman.
  • Tetapkan TTL konservatif untuk data sensitif atau cepat berubah.
  • Hubungkan invalidasi ke event perubahan data, bukan hanya waktu.
  • Tambahkan structured logging untuk cache_key, hit/miss, dan context request.
  • Buat test isolasi antar entitas, tenant, dan viewer context.
  • Audit seluruh lapisan cache, bukan hanya cache aplikasi.
  • Pertimbangkan untuk tidak men-cache bagian yang sensitif bila manfaat performanya kecil.

Penutup

Kasus debug backend: salah cache key sering terlihat sepele karena aplikasi tetap merespons normal. Padahal, untuk data donasi, afiliasi, atau relasi politik yang sensitif, bug ini bisa berubah menjadi insiden privasi dan reputasi. Solusi yang benar bukan sekadar flush cache, melainkan mendesain key berdasarkan konteks penuh, memperbaiki invalidasi, memperpendek blast radius dengan TTL yang masuk akal, dan menambah test serta observabilitas yang tepat.

Jika Anda menangani endpoint profil publik, anggap cache sebagai bagian dari data correctness, bukan hanya optimasi performa. Di area sensitif, hasil yang cepat tetapi salah lebih berbahaya daripada hasil yang sedikit lebih lambat namun benar.