Caching Strategies

Apa itu Caching?

Secara sederhana, caching adalah proses menyimpan salinan data di lokasi penyimpanan sementara (yang disebut cache) yang memiliki kecepatan akses lebih tinggi daripada sumber data aslinya. Tujuannya adalah untuk mempercepat permintaan data di masa mendatang dan mengurangi beban pada sistem utama (seperti database).


## 1. Cache Aside (Lazy Loading)

Ini adalah strategi caching yang paling umum dan intuitif. Aplikasi bertanggung jawab penuh untuk mengelola data antara cache dan database.

Cara Kerja:

  • Pembacaan (Read):

    1. Aplikasi pertama-tama mencari data di cache.

    2. Jika data ditemukan (Cache Hit), data tersebut langsung dikembalikan ke klien.

    3. Jika data tidak ditemukan (Cache Miss), aplikasi akan mengambil data dari database.

    4. Setelah data didapat dari database, aplikasi akan menyimpannya ke dalam cache untuk permintaan berikutnya.

    5. Data tersebut kemudian dikembalikan ke klien.

  • Penulisan (Write): Aplikasi menulis data langsung ke database dan biasanya akan menghapus data yang sesuai di cache (invalidasi).

Kelebihan:

  • Resilien: Jika cache mengalami kegagalan, aplikasi masih bisa berjalan dengan mengambil data langsung dari database, meskipun lebih lambat.

  • Model Data Sederhana: Cache hanya menyimpan data yang benar-benar diminta, sehingga tidak terbebani oleh data yang jarang diakses.

  • Mudah Diimplementasikan: Logikanya cukup sederhana dan berada sepenuhnya di level aplikasi.

Kekurangan:

  • Latency pada Cache Miss: Permintaan pertama untuk data yang belum ada di cache akan lebih lambat karena harus melalui tiga langkah: cek cache, ambil dari DB, dan simpan ke cache.

  • Data Basi (Stale Data): Ada kemungkinan data di cache tidak sinkron dengan di database jika ada pembaruan di database yang tidak diikuti dengan invalidasi cache.

Contoh Implementasi (Go, GORM, Redis):

Anggap kita memiliki redisClient untuk interaksi dengan Redis dan db untuk GORM.

package repository

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"github.com/go-redis/redis/v8"
	"gorm.io/gorm"
)

type User struct {
	ID   uint   `gorm:"primaryKey"`
	Name string
}

type UserRepository struct {
	db          *gorm.DB
	redisClient *redis.Client
}

// GetUserByID implements Cache Aside strategy
func (r *UserRepository) GetUserByID(ctx context.Context, id uint) (*User, error) {
	cacheKey := fmt.Sprintf("user:%d", id)

	// 1. Coba ambil dari cache terlebih dahulu
	val, err := r.redisClient.Get(ctx, cacheKey).Result()
	if err == nil {
		// Cache Hit
		user := &User{}
		if json.Unmarshal([]byte(val), user) == nil {
			return user, nil
		}
	}

	// 2. Cache Miss, ambil dari database
	user := &User{}
	if err := r.db.WithContext(ctx).First(user, id).Error; err != nil {
		return nil, err // User tidak ditemukan di DB
	}

	// 3. Simpan hasil ke cache untuk permintaan berikutnya
	userJSON, _ := json.Marshal(user)
	r.redisClient.Set(ctx, cacheKey, userJSON, time.Minute*10) // Set TTL (Time-To-Live)

	return user, nil
}

// UpdateUser - Invalidate cache on write
func (r *UserRepository) UpdateUser(ctx context.Context, user *User) error {
    if err := r.db.WithContext(ctx).Save(user).Error; err != nil {
        return err
    }

    // Invalidate cache
    cacheKey := fmt.Sprintf("user:%d", user.ID)
    r.redisClient.Del(ctx, cacheKey)

    return nil
}

## 2. Read Through

Strategi ini mirip dengan Cache Aside, namun logikanya dipindahkan dari aplikasi ke cache provider itu sendiri. Aplikasi hanya berinteraksi dengan cache.

Cara Kerja:

  1. Aplikasi meminta data dari cache.

  2. Jika data ada (Cache Hit), cache mengembalikannya.

  3. Jika data tidak ada (Cache Miss), cache provider (bukan aplikasi) akan mengambil data dari database, menyimpannya di cache, lalu mengembalikannya ke aplikasi.

Kelebihan:

  • Logika Aplikasi Lebih Bersih: Aplikasi tidak perlu tahu dari mana data berasal (cache atau DB). Cukup bertanya ke cache.

  • Konsistensi Logika: Logika pengambilan data terpusat di cache provider.

Kekurangan:

  • Ketergantungan pada Fitur Provider: Membutuhkan cache provider yang mendukung fungsionalitas ini (misalnya, Redis dengan modul tambahan atau cache custom). Implementasi out-of-the-box pada library Redis standar tidak ada.

  • Lebih Kompleks di Awal: Konfigurasi cache provider menjadi lebih rumit.

Contoh Implementasi (Go - Konseptual):

Karena ini bergantung pada provider, kode di level aplikasi akan terlihat sangat sederhana. Logika utamanya berada di dalam implementasi CacheService.

// Di level aplikasi, kodenya menjadi sangat simpel
func (s *UserService) GetUser(ctx context.Context, id uint) (*User, error) {
    // Aplikasi hanya tahu cara meminta data dari cache service.
    // Cache service yang akan handle logic read-through ke database jika miss.
    return s.cacheService.GetUser(ctx, id)
}

// --- Di dalam implementasi CacheService (KONSEP) ---
type CacheService struct {
    redisClient *redis.Client
    db *gorm.DB // Punya akses ke DB
}

func (cs *CacheService) GetUser(ctx context.Context, id uint) (*User, error) {
    cacheKey := fmt.Sprintf("user:%d", id)
    val, err := cs.redisClient.Get(ctx, cacheKey).Result()
    if err == nil {
        // ... unmarshal dan return
    }

    // Cache Miss: CacheService yang mengambil ke DB
    user := &User{}
    if err := cs.db.WithContext(ctx).First(user, id).Error; err != nil {
        return nil, err
    }
    
    // ... simpan ke redis dan return
    return user, nil
}

## 3. Write Through

Strategi ini memastikan data di cache dan database selalu sinkron saat operasi tulis.

Cara Kerja:

  1. Aplikasi menulis data ke cache.

  2. Cache provider secara sinkron (menunggu hingga selesai) menulis data tersebut ke database.

  3. Operasi tulis dianggap selesai hanya setelah data berhasil disimpan di cache DAN di database.

Kelebihan:

  • Konsistensi Data Tinggi: Data di cache dan database selalu sama setelah operasi tulis berhasil. Pembacaan tidak akan pernah mendapatkan data basi.

  • Beban Baca Terjamin Rendah: Karena data selalu ada di cache setelah ditulis, operasi baca berikutnya dijamin cache hit.

Kekurangan:

  • Latency Tulis Tinggi: Aplikasi harus menunggu konfirmasi dari dua sistem (cache dan database), sehingga proses penulisan menjadi lebih lambat.

  • Tidak Mengurangi Beban Tulis ke DB: Setiap operasi tulis tetap akan membebani database.

Contoh Implementasi (Go):

func (r *UserRepository) UpdateUserWriteThrough(ctx context.Context, user *User) error {
	// Untuk memastikan atomicity, gunakan transaksi database
	tx := r.db.WithContext(ctx).Begin()
	if tx.Error != nil {
		return tx.Error
	}

	// 1. Tulis ke database
	if err := tx.Save(user).Error; err != nil {
		tx.Rollback() // Batalkan jika gagal
		return err
	}

	// 2. Tulis ke cache
	userJSON, _ := json.Marshal(user)
	cacheKey := fmt.Sprintf("user:%d", user.ID)
	if err := r.redisClient.Set(ctx, cacheKey, userJSON, time.Minute*10).Err(); err != nil {
		tx.Rollback() // Batalkan jika gagal simpan ke cache
		return err
	}

	// Jika semua berhasil, commit transaksi
	return tx.Commit().Error
}

## 4. Write Back (Write Behind)

Strategi ini mengutamakan kecepatan tulis dengan menunda penulisan ke database.

Cara Kerja:

  1. Aplikasi menulis data hanya ke cache. Operasi dianggap selesai dan aplikasi mendapat respons cepat.

  2. Cache akan menandai data ini sebagai "kotor" (dirty).

  3. Secara asinkron (di latar belakang), setelah beberapa waktu atau dalam batch, cache akan menulis data yang "kotor" tersebut ke database.

Kelebihan:

  • Latency Tulis Sangat Rendah: Aplikasi tidak perlu menunggu penulisan ke database. Sangat cocok untuk aplikasi dengan beban tulis yang sangat tinggi.

  • Mengurangi Beban Tulis DB: Beberapa pembaruan pada data yang sama dalam waktu singkat dapat digabungkan menjadi satu kali penulisan ke database.

Kekurangan:

  • Risiko Kehilangan Data: Jika cache mati atau mengalami crash sebelum data sempat ditulis ke database, data tersebut akan hilang permanen.

  • Kompleksitas Implementasi: Membutuhkan mekanisme antrian (queue) atau worker di latar belakang untuk memproses penulisan ke database.

Contoh Implementasi (Go - Konseptual):

// Aplikasi hanya menulis ke cache
func (r *UserRepository) UpdateUserWriteBack(ctx context.Context, user *User) error {
    userJSON, _ := json.Marshal(user)
    cacheKey := fmt.Sprintf("user:%d", user.ID)

    // 1. Tulis ke cache utama
    if err := r.redisClient.Set(ctx, cacheKey, userJSON, time.Minute*10).Err(); err != nil {
        return err
    }

    // 2. Tambahkan ID ke antrian 'dirty' untuk diproses worker
    return r.redisClient.SAdd(ctx, "dirty_users", user.ID).Err()
}

// Di tempat lain, sebuah worker/background job akan berjalan:
func ProcessDirtyUsersWorker(db *gorm.DB, redisClient *redis.Client) {
    for {
        // Ambil satu ID dari set 'dirty_users'
        idStr, err := redisClient.SPop(context.Background(), "dirty_users").Result()
        if err != nil {
            time.Sleep(5 * time.Second) // Tunggu jika antrian kosong
            continue
        }
        
        id, _ := strconv.Atoi(idStr)
        cacheKey := fmt.Sprintf("user:%d", id)

        // Ambil data terbaru dari cache
        val, _ := redisClient.Get(context.Background(), cacheKey).Result()
        
        user := &User{}
        json.Unmarshal([]byte(val), user)

        // Tulis ke database
        db.Save(user)
    }
}

## 5. Write Around

Strategi ini memprioritaskan agar cache tidak diisi oleh data yang mungkin jarang dibaca.

Cara Kerja:

  1. Operasi tulis (Create/Update) dilakukan langsung ke database, sepenuhnya melewati (around) cache.

  2. Data hanya akan masuk ke cache saat ada operasi baca yang mengalami Cache Miss (sama seperti alur baca pada Cache Aside).

Kelebihan:

  • Cache Efisien: Mencegah cache dipenuhi data yang baru ditulis tapi mungkin tidak akan pernah dibaca kembali. Cocok untuk data log atau arsip.

  • Latency Tulis Baik: Sama seperti menulis langsung ke database, tidak ada overhead penulisan ke cache.

Kekurangan:

  • Latency Baca Tinggi untuk Data Baru: Permintaan baca sesaat setelah data ditulis akan selalu menjadi Cache Miss, sehingga lebih lambat.

Contoh Implementasi (Go):

Kombinasi dari fungsi baca Cache Aside dan fungsi tulis yang hanya ke DB.

// Fungsi baca sama persis dengan Cache Aside
func (r *UserRepository) GetUserByID(ctx context.Context, id uint) (*User, error) {
    // ... (logic Cache Aside seperti contoh #1)
}

// Fungsi tulis hanya ke database, melewati cache
func (r *UserRepository) UpdateUserWriteAround(ctx context.Context, user *User) error {
	// Langsung simpan ke database
	return r.db.WithContext(ctx).Save(user).Error
}

Ringkasan dan Kapan Menggunakannya

Strategi
Kapan Digunakan
Keuntungan Utama
Kerugian Utama

Cache Aside

Kasus umum, read-heavy, toleran data basi sesaat

Fleksibel, Resilien

Latency pada cache miss

Read Through

Ingin logika aplikasi bersih, read-heavy

Kode aplikasi simpel

Butuh dukungan provider

Write Through

Data kritis, butuh konsistensi tinggi (cth: data finansial)

Konsistensi data tinggi

Latency tulis tinggi

Write Back

Aplikasi write-heavy, butuh performa tulis super cepat

Latency tulis sangat rendah

Risiko kehilangan data

Write Around

Data ditulis sekali, jarang dibaca (cth: log, analitik)

Cache tidak "tercemar"

Latency baca tinggi untuk data baru

Konteks Proyek (Go, Postgres, GORM, Docker)

Dalam lingkungan Docker, Anda akan menjalankan beberapa kontainer:

  1. Aplikasi Go Anda: Berisi logika bisnis dan implementasi strategi caching yang dipilih.

  2. Database PostgreSQL: Sumber data utama.

  3. Cache Server (misalnya, Redis): Penyimpanan cache.

Anda dapat mendefinisikannya dalam file docker-compose.yml:

version: '3.8'
services:
  go-app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=postgres
      - REDIS_HOST=redis
    depends_on:
      - postgres
      - redis

  postgres:
    image: postgres:17-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydb
    ports:
      - "5432:5432"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

Untuk proyek Anda, Cache Aside adalah titik awal yang paling umum dan paling aman untuk diimplementasikan karena keseimbangan antara kemudahan implementasi dan manfaat performa yang didapat.

Last updated