๐ฌ 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
Memulai Program dengan Hello World
Dalam seri Golang Basic ini, kita akan memulai dengan program Hello World. Program ini adalah gerbang pertama dalam mempelajari bahasa pemrograman dan telah menjadi tradisi sejak diinisiasi oleh Brian Kernighan bertahun-tahun yang lalu. Struktur Prog...
๐ง Latihan Pointer di Golang
โ Soal 1 - Dasar Pointer Deskripsi:Buat program yang: Membuat variabel angka dengan nilai 5 Menampilkan alamat dari variabel tersebut Menyimpan alamatnya ke variabel pointer Tampilkan nilai dari pointer Contoh Output: Alamat dari angka: 0xc0000...
Aplikasi Todo (CLI)
Tujuan Membuat aplikasi Command Line Interface (CLI) untuk mengelola daftar tugas langsung dari terminal. Contoh perintah dasar: tasks Fitur Utama Aplikasi harus mampu melakukan operasi CRUD pada data tugas yang disimpan dalam file. Operasi-operasi ...