π¬ Golang Ticket Booking System from Scratch β No Framework (Part 3)
π§ Booking Service & Race Condition Simulation
Di part 3 ini, kita akan membuat service untuk proses booking kursi, menambahkan unit test, dan melakukan simulasi race condition agar memastikan 1 kursi hanya bisa dibooking oleh 1 orang meskipun ada banyak request bersamaan.
π§ Struktur File Project
Untuk kamu yang mengikuti seri ini, kita sudah punya struktur seperti berikut:
ticket-booking/
βββ cmd/api/main.go # HTTP server (nanti)
βββ internal/
β βββ service/
β βββ booking_service.go # BookingService kita buat sekarang
βββ repository/
β βββ booking_repository.go
β βββ booking_repository_test.go1. Membuat BookingService
Kita akan buat interface BookingService dan implementasinya di service/booking_service.go.
π¨ File: service/booking_service.go
package service
import (
"context"
"database/sql"
"errors"
"github.com/fardannozami/ticket-booking/helper"
"github.com/fardannozami/ticket-booking/repository"
)
type BookingService interface {
BookSeat(ctx context.Context, eventId, seatId, userId int) (int, error)
}
type bookingService struct {
Db *sql.DB
BookingRepository repository.BookingRepository
}
func NewServiceRepository(db *sql.DB, bookingRepository repository.BookingRepository) BookingService {
return &bookingService{
Db: db,
BookingRepository: bookingRepository,
}
}π‘ Penjelasan:
BookSeatadalah fungsi utama untuk booking.- Kita mulai dengan membuka transaksi, lalu:
- Cek apakah seat masih
"AVAILABLE" - Tandai seat sebagai
"BOOKED" - Kurangi kuota event
- Simpan data booking
π§ Implementasi BookSeat
func (service *bookingService) BookSeat(ctx context.Context, eventId int, seatId int, userId int) (int, error) {
tx, err := service.Db.BeginTx(ctx, nil)
helper.PanicIfError(err)
defer func() {
r := recover()
if r != nil {
tx.Rollback()
panic(r)
}
}()
status := service.BookingRepository.GetSeatStatus(ctx, tx, seatId)
if status != "AVAILABLE" {
tx.Rollback()
return 0, errors.New("seat already booked")
}
service.BookingRepository.MarkSeatAsBooked(ctx, tx, seatId)
service.BookingRepository.DecrementEventQuota(ctx, tx, eventId)
id := service.BookingRepository.InsertBooking(ctx, tx, eventId, seatId, userId)
return id, tx.Commit()
}2. Unit Test BookingService
π§ͺ File: service/booking_repository_test.go
func TestBookSeat(t *testing.T) {
inserUser(1, "Ajitama")
repo := repository.NewBookingRepository()
service := NewServiceRepository(db, repo)
ctx := context.Background()
id, err := service.BookSeat(ctx, 1, 2, 3)
assert.NoError(t, err)
var eventId, seatId, userId int
err = db.QueryRowContext(ctx, "SELECT event_id, seat_id, user_id FROM bookings WHERE id = ?", id).Scan(&eventId, &seatId, &userId)
assert.Equal(t, 1, eventId)
assert.Equal(t, 2, seatId)
assert.Equal(t, 3, userId)
}π‘ Penjelasan:
- Kita pastikan booking berhasil.
- Data di database sesuai input (eventId, seatId, userId).
3. Simulasi Race Condition
Untuk memastikan hanya 1 user yang bisa booking kursi walaupun ada 50 goroutine, kita buat test berikut:
π₯ File: service/booking_repository_test.go
func TestBookSeatRaceCondition(t *testing.T) {
repo := repository.NewBookingRepository()
service := NewBookingService(db, repo)
ctx := context.Background()
totalUser := 50
for i := 1; i <= totalUser; i++ {
inserUser(i, fmt.Sprintf("user %d", i))
}
var wg sync.WaitGroup
type bookingResult struct {
userId int
err error
}
result := make(chan bookingResult, totalUser)
wg.Add(totalUser)
for i := 1; i <= totalUser; i++ {
go func(userId int) {
defer wg.Done()
_, err := service.BookSeat(ctx, 1, 2, userId)
result <- bookingResult{userId: userId, err: err}
}(i)
}
wg.Wait()
close(result)
var successUsers []int
for res := range result {
if res.err == nil {
successUsers = append(successUsers, res.userId)
fmt.Printf("[SUCCESS] User dengan ID %d berhasil booking seat\n", res.userId)
} else {
fmt.Printf("[FAILED] User dengan ID %d gagal booking seat\n", res.userId)
}
}
assert.Equal(t, 1, len(successUsers), "Harusnya hanya satu user yang berhasil booking")
}π§ Kenapa Bisa Aman?
Karena:
- Kita menggunakan transaksi database (
BEGINβCOMMIT) - Kita cek status kursi di awal transaksi, dan hanya lanjut jika seat
"AVAILABLE" - Bila dua transaksi bersamaan, salah satu pasti gagal saat mencoba update
β Kesimpulan
- Kita telah berhasil membuat
BookingServiceyang menggunakan transaksi SQL. - Kita menambahkan unit test dan race condition simulation.
- Terbukti hanya 1 user yang berhasil booking meskipun ada 50 goroutine serentak!
More Articles
You might also like
Package, Import, dan Access Modifier di Golang
Halo teman-teman developer πHari ini kita akan bahas salah satu fondasi penting dalam bahasa Go: package, import, dan access modifier. Ini adalah konsep dasar yang wajib banget kamu kuasai sebelum masuk ke hal-hal yang lebih kompleks seperti concurr...
Panduan Instalasi Golang di WSL 2 Ubuntu
Halo teman-teman! Pada tutorial ini, kita akan belajar cara menginstal Golang di WSL 2 Ubuntu dengan langkah-langkah yang mudah dipahami. Yuk, kita mulai! 1. Mengatur WSL ke Versi 2 Sebelum menginstal Golang, pastikan WSL (Windows Subsystem for Linux...
Package strings dan strconv di Golang
Setelah sebelumnya membahas fmt, errors, os, dan flag, kali ini kita akan membahas dua package penting lainnya dalam standard library Go, yaitu strings dan strconv. Keduanya sangat berguna untuk memproses dan memanipulasi string dan konversi data β k...