๐ฌ 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:
* BookSeat adalah 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 BookingService yang 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
Pengenalan Channel di Golang
Halo teman-teman! Kembali lagi di seri tutorial Golang Goroutine.Setelah sebelumnya kita belajar membuat Goroutine, sekarang kita akan membahas Channel, fitur penting di Go yang membuat komunikasi antar Goroutine jadi aman dan efisien. Di artikel ini...
Doctor Booking App - Part 1
Halo! Pada kesempatan kali ini, saya akan membagikan proses awal pembuatan project Doctor Booking App menggunakan Laravel. Kita akan menggunakan beberapa tools modern seperti Livewire, Filament Admin, dan Pest untuk testing. Yuk langsung mulai! 1. Me...
Memahami Channel Direction di Golang
Di seri sebelumnya, kita sudah belajar tentang Select statement. Kali ini kita akan bahas fitur keren lainnya di Golang: Channel Direction. Apa Itu Channel Direction? Biasanya, channel di Go bisa digunakan untuk mengirim dan menerima data.Tapi, deng...