Alif Akbar.
Proyek/Studi Kasus

API Tiket Event: Arsitektur Backend Skala Besar untuk Flash Sale

Membangun arsitektur backend tiket berkonkurensi tinggi di Go untuk menangani lalu lintas burst besar-besaran tanpa race condition.

Go (Golang)PostgreSQLJWT & BCryptSwagger

Ringkasan Eksekutif: Sistem backend tiket event ini memecahkan masalah race condition dalam penjualan kilat (flash-sale) berkonkurensi tinggi. Dengan menggabungkan goroutine Go yang ringan dengan baris penguncian (row-level locking) PostgreSQL secara pesimis (FOR UPDATE SKIP LOCKED), sistem secara matematis menjamin integritas alokasi kursi tanpa mengorbankan latensi.

Masalah Bisnis: Mimpi Buruk Over-Booking

Penjualan tiket kilat (flash sales) adalah salah satu tantangan rekayasa paling brutal dalam e-commerce. Sistem yang diam dan nyaris tidak ada lalu lintas dapat seketika menerima 50.000 permintaan per detik begitu penghitung waktu mencapai nol. Tantangan utamanya bukanlah melayani permintaan HTTP secara asinkron, melainkan status (state) yang harus dikelola.

Jika sebuah stadion hanya memiliki 1.000 kursi yang tersisa, dan 50 pengguna secara bersamaan mengklik 'Beli' untuk 1 kursi spesifik yang sama di baris terdepan, database dan aplikasi Anda harus memastikan 1 orang berhasil dan 49 orang ditolak. Dalam arsitektur database yang naif, tanpa penguncian (locking) yang ketat, kondisi balapan (race condition) akan terjadi—mengakibatkan double-booking, yang secara finansial dan reputasi menghancurkan penyelenggara acara.

Dalam penjualan tiket (flash sales), sistem dapat mengalami lonjakan lalu lintas yang tiba-tiba dari nol menjadi puluhan ribu permintaan per detik. Tantangan utamanya bukan sekadar menyajikan halaman web dengan cepat, melainkan mencegah race conditions selama alokasi kursi. Jika 50 pengguna secara bersamaan mencoba membeli 1 kursi terakhir yang tersedia, database dan lapisan backend harus menjamin secara matematis bahwa hanya tepat 1 transaksi yang berhasil, sementara 49 lainnya ditolak secara instan.

Solusi Arsitektur

sequenceDiagram
    participant UserA as User A (Go Thread 1)
    participant UserB as User B (Go Thread 2)
    participant PG as PostgreSQL

    UserA->>PG: BEGIN TRANSACTION
    UserB->>PG: BEGIN TRANSACTION
    
    UserA->>PG: SELECT * FROM seats WHERE id=5 FOR UPDATE SKIP LOCKED
    Note right of PG: Row ID=5 is Locked by Thread 1
    
    UserB->>PG: SELECT * FROM seats WHERE id=5 FOR UPDATE SKIP LOCKED
    Note right of PG: Thread 2 skips immediately. Returns 0 rows.
    
    PG-->>UserB: Seat Unavailable (Fast Fail)
    UserB->>UserB: Return HTTP 409 Conflict
    
    PG-->>UserA: Returns Seat Data
    UserA->>PG: UPDATE seats SET status='reserved' WHERE id=5
    UserA->>PG: COMMIT
    Note right of PG: Row ID=5 Unlocked
    UserA->>UserA: Return HTTP 201 Success

1. Eliminasi Race Condition (Snippet Go)

Untuk menjamin integritas transaksi kursi, saya mengimplementasikan kontrol konkurensi pesimis menggunakan row-level locking pada PostgreSQL. Secara spesifik, menggunakan klausa `FOR UPDATE SKIP LOCKED`. Ini memastikan bahwa ketika sebuah thread Go mencoba membaca kursi, database akan mengunci baris (row) tersebut. Jika thread Go lain mencoba memesan kursi yang sama secara serentak, ia tidak akan menunggu dalam antrean (yang dapat memblokir server Go), melainkan langsung melompatinya (skip) dan mengembalikan respons kegagalan yang cepat (fast-fail) kepada klien.

func (repo *TicketRepository) ReserveSeat(ctx context.Context, tx *sql.Tx, seatID int, userID int) error {
    // 1. Attempt to lock the seat exclusively without waiting
    var status string
    err := tx.QueryRowContext(ctx, `
        SELECT status 
        FROM seats 
        WHERE id = $1 AND status = 'available' 
        FOR UPDATE SKIP LOCKED`, seatID).Scan(&status)

    if err != nil {
        if err == sql.ErrNoRows {
            // Seat is either already booked, or currently locked by another transaction
            return ErrSeatUnavailable
        }
        return err
    }

    // 2. We have the lock! Safely mutate the state.
    _, err = tx.ExecContext(ctx, `
        UPDATE seats 
        SET status = 'reserved', user_id = $2 
        WHERE id = $1`, seatID, userID)
        
    return err
}

Momen Kritis: Kelelahan Pool Koneksi (Connection Pool Exhaustion)

Goroutine Go yang super cepat menciptakan masalah baru di lapisan infrastruktur. Saat saya melakukan load-testing sistem dengan 10.000 concurrent user (CCU), Go API sama sekali tidak berkeringat (CPU hanya 15%). Namun, database PostgreSQL langsung mogok karena connection refused.

Aplikasi Go mencoba membuka ribuan koneksi TCP ke PostgreSQL secara bersamaan. PostgreSQL secara default hanya dapat menangani ~100 koneksi bersamaan sebelum mati lemas. Solusinya membutuhkan penyesuaian keras pada variabel `database/sql` bawaan Go (`SetMaxOpenConns` ke nilai tetap yang tidak akan menenggelamkan DB) dikombinasikan dengan penerapan PgBouncer di depan PostgreSQL untuk me-multiplex koneksi transaksi.

Hasil yang Terukur (Benchmarks)

MetricNaive Architecture (PHP/MySQL)Current Architecture (Go/PG/PgBouncer)
Throughput (Req/Sec)~250 RPS12,500+ RPS
Double-Booking Rate3.5% under load0.00% (Mathematically impossible)
Peak API Memory Usage3.5 GB~110 MB
Response Latency (p99)6,500ms (Timeouts)~35ms