๐ฌ 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
Tipe Data Array, Slice, dan Map di Golang
Setelah sebelumnya kita sudah membahas tipe data string, boolean dan number, sekarang kita lanjutkan dengan beberapa tipe data penting yang sering digunakan, yaitu Array, Slice, dan Map. Array digunakan untuk menyimpan sejumlah elemen dengan panjan...
Go Financial Core System - System Design
Kita mau bikin sistem seperti e-wallet (contoh: Dana, OVO), tapi: Aman (tidak bisa saldo dobel / hilang) Tidak error saat banyak orang transaksi bersamaan Mudah dikembangkan ๐ง 1. Gambaran Besar
Struct dan Struct Method di Golang
Halo teman-teman, kali ini kita bakal bahas salah satu fitur penting di Golang yang sering banget dipakai di dunia backend, yaitu struct dan struct method. Kalau kamu udah pernah pakai OOP di bahasa lain kayak Java atau Python, kamu bisa nganggep str...