Stateless vs Stateful #

Ketika kita merancang dan membangun arsitektur aplikasi berskala besar untuk lingkungan cloud, salah satu keputusan desain paling awal dan paling berdampak adalah menentukan apakah aplikasi kita akan bersifat Stateless (Tanpa State) atau Stateful (Dengan State). Perbedaan antara kedua model ini mungkin hampir tidak terasa ketika aplikasi kita masih kecil dan berjalan di atas satu server virtual tunggal. Namun, begitu sistem kita mulai bertumbuh dan kita dipaksa untuk menerapkan penskalaan horizontal otomatis (auto-scaling), melakukan rilis aplikasi tanpa downtime (zero-downtime deployment), atau merancang ketahanan bencana (disaster recovery), pemisahan ini akan menjadi batas pembeda utama antara sistem yang fleksibel dan sistem yang rapuh. Arsitektur stateless merupakan pilar terpenting yang memungkinkan elastisitas dan resiliensi cloud-native dapat terwujud secara efektif.

Anatomi Perbedaan: Stateless vs Stateful #

Sebelum kita melangkah lebih jauh ke dalam pola implementasi, mari kita bedah definisi teknis dari kedua konsep ini. Di dalam rekayasa perangkat lunak, State didefinisikan sebagai data atau informasi sesi pengguna yang wajib disimpan oleh aplikasi untuk diingat kembali di antara satu transaksi request dan request berikutnya.

Stateless Architecture adalah model desain di mana setiap request yang dikirimkan oleh pengguna (client) bersifat mandiri (self-contained). Server tidak menyimpan memori, konteks, atau data sesi apa pun mengenai pengguna tersebut setelah transaksi selesai diproses. Setiap request baru yang masuk wajib membawa seluruh informasi identifikasi yang dibutuhkan secara lengkap. Konsekuensinya, Load Balancer bebas mengarahkan request berikutnya dari pengguna yang sama ke server mana pun (Server-A, Server-B, atau Server-C) secara dinamis tanpa takut terjadi error kegagalan sesi.

Stateful Architecture adalah model desain di mana server berkewajiban menyimpan data sesi, konteks historis, atau status transaksi pengguna di dalam memori internal lokalnya (seperti RAM lokal atau penyimpanan disk lokal instans). Request berikutnya dari pengguna yang sama wajib diarahkan ke server fisik yang sama yang memegang data sesi tersebut. Jika Load Balancer mengarahkan request pengguna ke server lain, transaksi akan gagal karena server baru tidak memiliki “konteks memori” historis pengguna tersebut.

Tabel di bawah ini membandingkan karakteristik operasional kedua arsitektur secara mendalam:

Kriteria Perbandingan Stateless Architecture Stateful Architecture
Lokasi Penyimpanan Sesi Disimpan di sisi client (token) atau database cache eksternal terpusat. Disimpan di memori lokal RAM / disk server instans itu sendiri.
Ketergantungan Load Balancer Bebas merutekan request ke instans mana saja (round-robin murni). Wajib mengaktifkan Sticky Sessions (Session Affinity) agar rute terkunci.
Skalabilitas Horizontal Sangat Mudah (Tinggal menambah server baru tanpa sinkronisasi). Sangat Sulit (Butuh migrasi state atau sinkronisasi memori lintas server).
Dampak Kerusakan Server Nol (Request langsung dialihkan ke server sehat tanpa disadari user). Sesi pengguna aktif akan hilang, memicu error dan memaksa relogin.
Kemudahan Rolling Update Sangat Cepat & Aman (Bisa langsung mematikan server lama). Lambat (Butuh menunggu user log out atau memindahkan sesi secara manual).
Penggunaan Resource Server Konstan dan dapat diprediksi karena server tidak menyimpan data sesi. Meningkat linear dengan jumlah pengguna aktif di server tersebut.
Kompleksitas Kode Aplikasi Sedikit lebih kompleks karena butuh integrasi adapter cache eksternal. Sangat sederhana di awal karena memanfaatkan sesi bawaan web server.

Tantangan Sesi dan Metode Manajemen Sesi di Cloud #

Ketika kita memaksakan aplikasi yang stateful untuk berjalan di lingkungan cloud yang dinamis, kita akan berhadapan dengan masalah manajemen sesi pengguna. Ada tiga metode utama untuk menangani hal ini, masing-masing dengan konsekuensi operasionalnya sendiri.

1. Sticky Sessions (Session Affinity) #

Dalam metode ini, Load Balancer dipaksa untuk mengidentifikasi pengguna (biasanya melalui cookie khusus) dan selalu mengarahkan request berikutnya dari pengguna tersebut ke server fisik yang sama.

flowchart TD
    Client1["Pengguna 1"] --> LB["Load Balancer (Sticky)"]
    Client2["Pengguna 2"] --> LB
    
    LB -->|"Rute Dikunci (Cookie 1)"| ServerA["Server A (Menyimpan Sesi 1)"]
    LB -->|"Rute Dikunci (Cookie 2)"| ServerB["Server B (Menyimpan Sesi 2)"]
    
    style ServerA stroke:#0288d1,stroke-width:2px
    style ServerB stroke:#0288d1,stroke-width:2px

Mengapa Sticky Sessions adalah Anti-Pattern di Cloud?

  • Distribusi Beban Tidak Merata (Uneven Load): Jika sebagian besar pengguna aktif kebetulan dialokasikan ke Server A, Server A akan mengalami kelebihan beban (overloaded) sementara Server B tetap menganggur. Load Balancer tidak bisa memindahkan beban kerja tersebut secara adil.
  • Masalah Auto-Healing: Jika Server A mengalami crash hardware, platform cloud akan segera mematikan server tersebut dan menggantinya dengan yang baru. Namun, seluruh data sesi pengguna yang terkunci di Server A akan hilang selamanya. Pengguna akan mengalami error mendadak dan dipaksa untuk melakukan login ulang.
  • Hambatan Rolling Update: Saat kita ingin merilis versi kode baru, kita tidak bisa mematikan server lama begitu saja karena ada pengguna aktif yang sesinya tersimpan di sana. Kita harus menunggu sampai sesi mereka kedaluwarsa, yang memperlambat rilis kode program kita.

2. Session Replication (Sinkronisasi Memori) #

Metode ini mencoba memecahkan masalah ketersediaan dengan menyalin (replicate) setiap sesi yang dibuat di satu server ke seluruh server lain di dalam kluster.

flowchart TD
    ServerA["Server A (Sesi 1 & 2)"] -. "Replikasi Sesi via Multicast" .-> ServerB["Server B (Sesi 1 & 2)"]
    ServerB -. "Replikasi Sesi via Multicast" .-> ServerA
    
    style ServerA stroke:#d32f2f,stroke-width:2px
    style ServerB stroke:#d32f2f,stroke-width:2px

Mengapa Session Replication Gagal pada Skala Besar? Meskipun metode ini menghilangkan ketergantungan pada satu server, ia memicu kemacetan jaringan (network bottleneck) yang parah. Setiap kali seorang pengguna menambahkan item ke keranjang belanja di Server A, server tersebut harus menyiarkan data tersebut ke puluhan server lain. Pada kluster dengan 50 server, trafik jaringan untuk sinkronisasi sesi ini akan memakan bandwidth yang sangat besar, menyisakan sedikit ruang untuk trafik transaksi pengguna yang asli. RAM server juga cepat habis karena harus menampung duplikat sesi dari seluruh server.

3. Distributed Session Cache (Penyimpanan Sesi Terpusat) #

Ini adalah pola desain yang direkomendasikan untuk arsitektur modern. Server aplikasi kita dibuat sepenuhnya stateless. Data sesi dikeluarkan dari RAM lokal server dan disimpan ke dalam database cache eksternal yang cepat dan terpusat (seperti Redis cluster atau Memcached).

flowchart TD
    Client["Pengguna"] --> LB["Load Balancer"]
    LB -->|"Trafik Acak (Stateless)"| ServerA["Server A (Stateless Compute)"]
    LB -->|"Trafik Acak (Stateless)"| ServerB["Server B (Stateless Compute)"]
    
    ServerA -->|"Baca/Tulis Sesi"| Redis["Redis Cluster (Distributed Cache)"]
    ServerB -->|"Baca/Tulis Sesi"| Redis
    
    style Redis stroke:#2e7d32,stroke-width:2px

Dengan pola ini, jika Server A mati, Load Balancer dapat langsung mengalihkan request berikutnya ke Server B. Server B akan menyapa Redis untuk mengambil data sesi pengguna tersebut. Pengguna sama sekali tidak menyadari bahwa server di belakang layar baru saja berganti. Penskalaan horizontal dapat berjalan secara instan dan tanpa batas.


Mengapa Stateless adalah Fondasi Utama Cloud-Native? #

Membangun aplikasi serverless atau containerized dengan model stateless memberikan keunggulan resiliensi yang luar biasa berkat dukungan fitur-fitur cloud berikut:

1. Penskalaan Horizontal Instan (Horizontal Scaling) #

Ketika trafik aplikasi melonjak tiba-tiba, sistem auto-scaling akan mendeploy 10 server baru secara instan. Karena aplikasi bersifat stateless, server-server baru tersebut dapat langsung melayani trafik request pengguna detik itu juga tanpa perlu disinkronisasikan data sesinya terlebih dahulu. Kita cukup menunjuk Load Balancer untuk menyebarkan request ke server baru tersebut.

2. Pemulihan Mandiri Tanpa Efek (Auto-Healing) #

Jika sebuah instans server mengalami deadlock atau crash akibat kerusakan hardware, Load Balancer akan langsung menandainya sebagai unhealthy dan menghentikan pengiriman request ke sana. Request berikutnya dari pengguna akan dialihkan ke server yang sehat. Karena data sesi disimpan di luar server, pengguna tidak akan merasakan gangguan atau dipaksa melakukan login ulang.

3. Rilis Kode Tanpa Gangguan (Zero-Downtime Deployments) #

Dalam proses rolling update, platform cloud akan mematikan satu instans server versi lama dan menghidupkan satu instans server versi baru secara bertahap. Pada arsitektur stateless, proses pergantian ini berjalan mulus tanpa mengganggu transaksi pengguna yang sedang aktif berjalan.


Otentikasi Stateless: Pola Token JWT (JSON Web Token) #

Di samping menggunakan distributed cache (seperti Redis) untuk eksternalisasi state, arsitektur modern sering mengadopsi pola otentikasi stateless menggunakan JWT (JSON Web Token).

Dalam pola JWT, server aplikasi tidak menyimpan data sesi login pengguna sama sekali, baik di memori lokal maupun di database cache. Alur kerjanya adalah sebagai berikut:

sequenceDiagram
    participant Client as Pengguna (Browser)
    participant Web as Web Server (Stateless)
    
    Client->>Web: POST /login {username, password}
    Note over Web: Validasi Kredensial &<br/>Buat Token JWT dengan Tanda Tangan Kriptografi
    Web-->>Client: Kirim JWT Token
    
    Client->>Web: GET /profile (Header: Authorization Bearer <JWT>)
    Note over Web: Verifikasi Tanda Tangan JWT<br/>Secara Lokal Menggunakan Public/Private Key
    Web-->>Client: Kirim Data Profil (HTTP 200)
  1. Login: Pengguna mengirimkan username dan password ke server.
  2. Token Generation: Server memverifikasi kredensial. Jika benar, server membuat sebuah token JWT yang berisi data non-sensitif (user ID, nama, role) dan menandatanganinya menggunakan kunci kriptografi rahasia (secret key). Token dikirim balik ke client.
  3. Stateless Request: Client menyimpan token tersebut di memori browser (atau cookie). Setiap kali client mengirim request baru, token tersebut disisipkan di header otorisasi.
  4. Instant Verification: Server menerima token, melakukan verifikasi tanda tangan kriptografisnya menggunakan kunci rahasia. Jika valid, server langsung memproses data tanpa perlu melakukan query pencarian ke database sesi. Server bertindak sepenuhnya stateless.

Kelemahan JWT dan Mitigasinya: Kesulitan terbesar dari JWT stateless murni adalah melakukan pencabutan (revocation) token sebelum masa kedaluwarsanya habis (misalnya ketika user melakukan logout atau admin ingin menonaktifkan akun yang dicurigai). Karena verifikasi token dilakukan secara mandiri oleh server tanpa query database, server tidak tahu jika token tersebut seharusnya sudah tidak berlaku.

Untuk mengatasi ini, kita biasanya mengadopsi skema kombinasi:

  • Access Token: Berdurasi sangat pendek (misal: 15 menit), divalidasi secara stateless murni demi performa tinggi.
  • Refresh Token: Berdurasi panjang (misal: 7 hari), disimpan di database cache terpusat (stateful). Saat access token habis, client harus meminta access token baru menggunakan refresh token yang akan divalidasi ke database.

Hubungan Stateless dengan Scaling Jaringan: Penanganan WebSockets dan SSE #

Salah satu pertanyaan arsitektur yang paling sering muncul adalah: “Bagaimana kita bisa membuat sistem kita sepenuhnya stateless jika kita membutuhkan fitur real-time seperti WebSockets atau Server-Sent Events (SSE)?”

Protokol WebSockets bersifat stateful secara inheren karena ia mempertahankan koneksi TCP persisten yang terus terbuka antara browser client dengan server backend tertentu. Kita tidak bisa begitu saja memutuskan koneksi ini tanpa memutus aliran data real-time.

Untuk men-scale horizontal aplikasi WebSockets secara stateless di level compute layer, kita harus menggunakan pola Pub/Sub (Publish/Subscribe) Message Broker (seperti Redis Pub/Sub atau Apache Kafka) sebagai saluran komunikasi silang antar-server.

flowchart TD
    User1["Pengguna 1"] <-->|"Koneksi WebSockets 1"| ServerA["Server A"]
    User2["Pengguna 2"] <-->|"Koneksi WebSockets 2"| ServerB["Server B"]
    
    ServerA <-->|"Pub/Sub Channel"| RedisBroker["Redis Pub/Sub (Broker)"]
    ServerB <-->|"Pub/Sub Channel"| RedisBroker
    
    style RedisBroker stroke:#2e7d32,stroke-width:2px

Cara Kerja Skalabilitas WebSockets Stateless:

  1. Pengguna 1 terhubung ke Server A via WebSockets.
  2. Pengguna 2 terhubung ke Server B via WebSockets.
  3. Saat Pengguna 1 mengirim pesan ke Pengguna 2, pesan tersebut diterima oleh Server A. Karena Pengguna 2 berada di server yang berbeda, Server A tidak bisa mengirimkannya secara lokal.
  4. Server A mempublikasikan pesan tersebut ke channel Redis Pub/Sub.
  5. Server B yang telah berlangganan channel tersebut menerima pesan dari Redis, mendeteksi bahwa Pengguna 2 terhubung ke dirinya, dan mengirimkan pesan tersebut melalui koneksi WebSockets lokal ke Pengguna 2.

Dengan pendekatan ini, server aplikasi kita (Server A dan Server B) tetap dapat diperlakukan sebagai node komputasi stateless sekali pakai yang dapat ditambah atau dikurangi kapan saja, sementara keadaan koneksi jaringan dinamis dikoordinasikan secara eksternal.


Contoh Kode: Sesi Belanja Stateful vs Stateless #

Untuk memberikan pemahaman yang lebih konkret, mari kita bandingkan implementasi penanganan sesi keranjang belanja menggunakan Node.js Express.

Di bawah ini disajikan kode yang menunjukkan pendekatan stateful di dalam RAM lokal server (anti-pattern) beserta solusi stateless dengan Redis cache:

// anti-pattern-vs-solution.js
const express = require('express');
const Redis = require('ioredis');

const app = express();
app.use(express.json());

// =========================================================================
// ✗ ANTI-PATTERN: Menyimpan sesi belanja di memori RAM lokal server (Stateful)
// =========================================================================
// Jika kita menggunakan Load Balancer dengan 2 server, user akan kehilangan isi keranjang 
// setiap kali request mereka mendarat di server yang berbeda. RAM server juga rentan bocor.

const localCarts = {}; // Menyimpan data di RAM lokal node process (Stateful Trap!)

app.post('/cart/add-stateful', (req, res) => {
  const { userId, productId } = req.body;
  
  if (!userId || !productId) {
    return res.status(400).send({ error: "userId dan productId wajib diisi." });
  }

  // Inisialisasi keranjang jika belum ada di memori server ini
  if (!localCarts[userId]) {
    localCarts[userId] = [];
  }
  
  // Data disimpan di RAM lokal server ini saja
  localCarts[userId].push(productId); 
  
  res.status(200).send({ 
    message: "Item berhasil ditambahkan ke keranjang lokal (Stateful).",
    cart: localCarts[userId] 
  });
});


// =========================================================================
// ✓ BENAR: Pindahkan data sesi belanja ke database eksternal / Distributed Cache
// =========================================================================
// Server-A maupun Server-B dapat mati kapan saja tanpa merusak data keranjang belanja user.
// Server bertindak 100% stateless dan fokus hanya pada compute logic.

// Mengambil URL Redis dari Environment Variable (Mematuhi 12-Factor App)
const redisUrl = process.env.REDIS_URL || 'redis://127.0.0.1:6379';
const redisClient = new Redis(redisUrl);

redisClient.on('error', (err) => {
  console.error('Koneksi Redis Error:', err);
});

app.post('/cart/add-stateless', async (req, res) => {
  const { userId, productId } = req.body;
  
  if (!userId || !productId) {
    return res.status(400).send({ error: "userId dan productId wajib diisi." });
  }

  const cartKey = `cart:${userId}`;
  
  try {
    // Menyimpan data sesi belanja ke database cache terpusat Redis
    await redisClient.rpush(cartKey, productId);
    
    // Atur masa kedaluwarsa sesi belanja (misal: 2 jam / 7200 detik)
    // Ini menjamin RAM Redis tidak akan bocor karena data otomatis terhapus saat tidak aktif
    await redisClient.expire(cartKey, 7200);
    
    // Mengambil data terbaru dari Redis untuk dikembalikan ke client
    const currentCart = await redisClient.lrange(cartKey, 0, -1);
    
    res.status(200).send({ 
      message: "Item berhasil ditambahkan ke keranjang terpusat (Stateless).",
      cart: currentCart 
    });
  } catch (error) {
    console.error('Gagal menulis sesi ke Redis:', error);
    res.status(500).send({ error: "Gagal mengakses penyimpanan sesi terpusat." });
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Mengelola Stateful Workloads di Cloud secara Aman #

Meskipun stateless sangat dianjurkan untuk server aplikasi web, kita tidak mungkin menghindari model stateful secara mutlak di seluruh sistem kita. Data transaksional bisnis kita harus disimpan secara permanen di suatu tempat.

Prinsip arsitektur cloud yang baik adalah: isolasi stateful workload di layer khusus yang dirancang untuk itu, jangan campur adukkan dengan compute layer.

Beberapa best practice dalam mengelola stateful workload di cloud meliputi:

  • Managed DBaaS (Database as a Service): Serahkan manajemen state database kepada layanan managed dari cloud provider (seperti AWS RDS, Google Cloud SQL, atau DynamoDB) yang sudah memiliki sistem replikasi fisik otomatis, backup berkala, dan penanganan failover otomatis.
  • Stateful Container di Kubernetes: Jika kita terpaksa menjalankan sistem database sendiri di dalam kluster Kubernetes, gunakan objek StatefulSet (bukan Deployment biasa) yang dihubungkan dengan Persistent Volume Claims (PVC) guna menjamin ketika kontainer database mati, kontainer penggantinya akan ditempelkan kembali ke disk penyimpanan fisik yang sama secara otomatis tanpa kehilangan data.
  • Pemisahan Compute dan Storage: Hindari menyimpan berkas media (seperti foto profil pengguna) di dalam disk lokal VM. Gunakan layanan Object Storage (seperti AWS S3 atau Google Cloud Storage) yang memiliki tingkat ketahanan (durability) sangat tinggi dan dapat diakses secara paralel oleh ratusan server stateless kita.

Ringkasan #

  • Stateless berarti server tidak menyimpan data sesi di memori lokalnya antar transaksi request, sedangkan Stateful menyimpan memori sesi secara lokal yang mengunci rute Load Balancer.
  • Arsitektur stateless adalah prasyarat horizontal scaling instan, auto-healing tanpa interupsi, dan rilis aplikasi tanpa downtime (zero-downtime deployment).
  • Hindari menyimpan sesi di RAM lokal server aplikasi karena akan memicu kegagalan sesi saat server di-scale out atau diganti. Pindahkan state ke Distributed Cache (Redis) eksternal.
  • Gunakan token JWT untuk otentikasi stateless tingkat lanjut, yang memverifikasi keabsahan sesi pengguna menggunakan tanda tangan kriptografi tanpa query database.
  • Skalakan aplikasi real-time (WebSockets) dengan Pub/Sub menggunakan Redis atau Message Broker agar server aplikasi tetap stateless dan fleksibel.
  • Pisahkan compute layer dan storage layer secara tegas dengan meletakkan data dinamis pada layanan khusus seperti DBaaS, StatefulSet Kubernetes, dan Object Storage.

← Sebelumnya: Vendor Lock-In   Berikutnya: Horizontal vs Vertical Scaling →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact