Back to Blog

🎬 Golang Ticket Booking System from Scratch – No Framework (Part 2)

23 Juni 20253 min read

Pada part kedua dari seri "Ticket Booking System with Golang (Tanpa Framework)", kita akan mulai masuk ke bagian yang lebih "serius", yaitu:

* Menyelesaikan fungsi MarkSeatAsBooked

* Menambahkan unit test untuk fungsi tersebut

* Melakukan simulasi *race condition* agar 1 kursi tidak bisa dibooking lebih dari sekali secara bersamaan

* Menambahkan fitur penting lain: DecrementEventQuota dan InsertBooking

* Menambahkan constraint untuk mencegah duplikasi booking


πŸ“Œ Tujuan

Skenario umum yang sering terjadi dalam sistem booking adalah rebutan seat, misalnya saat beli tiket konser. Kita akan menggunakan transaksi dan locking dari database untuk mencegah race condition dan duplikasi booking.


✏️ Step 1 – Update Interface dan Implementasi MarkSeatAsBooked

πŸ“„ File: repository/booking_repository.go

βœ… Tambahkan Method pada Interface

go
type BookingRepository interface {
	GetSeatStatus(ctx context.Context, tx *sql.Tx, seatId int) string
	MarkSeatAsBooked(ctx context.Context, tx *sql.Tx, seatId int)
	DecrementEventQuota(ctx context.Context, tx *sql.Tx, eventId int)
	InsertBooking(ctx context.Context, tx *sql.Tx, userId, eventId, seatId int)
}

πŸ› οΈ Implementasi Fungsi MarkSeatAsBooked

go
func (repo *bookingRepository) MarkSeatAsBooked(ctx context.Context, tx *sql.Tx, seatId int) {
	SQL := "UPDATE seats SET status = 'BOOKED' WHERE id = ?"
	_, err := tx.ExecContext(ctx, SQL, seatId)
	helper.PanicIfError(err)
}

Penjelasan:

* Kursi akan ditandai sebagai "BOOKED"

* Dibungkus transaksi agar bisa rollback jika terjadi error


πŸ§ͺ Step 2 – Unit Test: Fungsi Sukses Booking Kursi

πŸ“„ File: repository/booking_repository_test.go

go
func TestMarkSeatAsBooked(t *testing.T) {
	insertEvent(1, 100, "Event 1")
	insertSeat(9, 1, "AVAILABLE", "A1")

	repo := NewBookingRepository()
	tx, err := db.BeginTx(context.Background(), nil)
	assert.NoError(t, err)

	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
		} else {
			tx.Commit()
		}
	}()

	repo.MarkSeatAsBooked(context.Background(), tx, 9)

	status := repo.GetSeatStatus(context.Background(), tx, 9)
	assert.Equal(t, "BOOKED", status)
}

πŸ”» Step 3 – Implementasi DecrementEventQuota

πŸ› οΈ Implementasi

go
func (repo *bookingRepository) DecrementEventQuota(ctx context.Context, tx *sql.Tx, eventId int) {
	SQL := "UPDATE events SET quota = quota - 1 WHERE id = ?"
	_, err := tx.ExecContext(ctx, SQL, eventId)
	helper.PanicIfError(err)
}

βœ… Unit Test

go
func TestDecrementEventQuota(t *testing.T) {
	insertEvent(1, 100, "Event 1")

	repo := NewBookingRepository()
	tx, err := db.BeginTx(context.Background(), nil)
	assert.NoError(t, err)

	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
		} else {
			tx.Commit()
		}
	}()

	repo.DecrementEventQuota(context.Background(), tx, 1)

	var quota int
	SQL := "SELECT quota FROM events WHERE id = ?"
	err = tx.QueryRowContext(context.Background(), SQL, 1).Scan(&quota)
	assert.NoError(t, err)
	assert.Equal(t, 99, quota)
}

🧾 Step 4 – Insert Booking

πŸ› οΈ Implementasi

go
func (repo *bookingRepository) InsertBooking(ctx context.Context, tx *sql.Tx, userId, eventId, seatId int) {
	SQL := "INSERT INTO bookings (user_id, event_id, seat_id) VALUES (?, ?, ?)"
	_, err := tx.ExecContext(ctx, SQL, userId, eventId, seatId)
	helper.PanicIfError(err)
}

βœ… Unit Test

go
func TestInsertBooking(t *testing.T) {
	repo := NewBookingRepository()

	tx, err := db.BeginTx(context.Background(), nil)
	assert.NoError(t, err)

	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
			t.Errorf("Unexpected panic: %v", r)
		} else {
			tx.Commit()
		}
	}()

	insertUser(1, "Fardan")
	repo.InsertBooking(context.Background(), tx, 1, 1, 9)

	var userId, eventId, seatId int
	SQL := "SELECT user_id, event_id, seat_id FROM bookings WHERE user_id = ? AND event_id = ? AND seat_id = ?"
	err = tx.QueryRowContext(context.Background(), SQL, 1, 1, 9).Scan(&userId, &eventId, &seatId)
	assert.NoError(t, err)
	assert.Equal(t, 1, userId)
	assert.Equal(t, 1, eventId)
	assert.Equal(t, 9, seatId)
}

πŸ› οΈ Step 5 – Insert Dummy User

go
func insertUser(id int, name string) {
	SQL := "INSERT INTO users (id, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name)"
	_, err := db.Exec(SQL, id, name)
	helper.PanicIfError(err)
}

🧩 Kesimpulan

Di part kedua ini kita sudah menyelesaikan:

* Fungsi booking dasar (MarkSeatAsBooked, DecrementEventQuota, InsertBooking)

* Menyiapkan unit test untuk memastikan semua berjalan aman

* Menambahkan proteksi unik di level database