Full-Stack

Court Reservation System

An online booking platform for sports courts — players pick a court and time, pay on their phone, and they're set. Built so a busy day never ends in a double-booking or a lost payment.

Year2024
ClientSimplilogi
RoleFull-Stack Developer
Duration6 months
Court Reservation System
Overview

Traditional sports-facility booking systems suffer from operational inefficiencies that frustrate both owners and customers. Manual processes lead to double-bookings, static pricing fails to optimise revenue, and clunky payment flows cause high abandonment. This platform automates operations, prices dynamically by demand, and delivers a mobile-first booking experience.

The challenge

The platform had to handle real-time availability across multiple courts, prevent booking conflicts, price dynamically by time and demand, integrate a payment gateway, and feel great on mobile — while staying scalable and maintainable with full type-safety.

The solution

A full-stack build: Next.js 16 frontend, Golang backend. Real-time availability with optimistic UI, a dynamic pricing engine, seamless Midtrans payments, and mobile-first responsive design. Redis caching keeps it fast; PostgreSQL handles persistence.

System design decisions

A booking app looks simple, but a few things can quietly break it: two people grabbing the same slot, a slow calendar, or a payment that gets lost halfway. Here are three of those moments and how I designed around each one.

01No double-booking

Making sure one slot can't be booked twice

PatternConcurrency control — pessimistic row locking, with a unique DB constraint as a backstop
The problemTwo people can tap the same open slot at the very same moment. If the app just asks "is it free?" and then saves, both get a "yes" — and now the same court is booked twice. One of them shows up to a court that's already taken.
What I didSo before a booking is saved, the database locks that exact slot so only one request can touch it at a time. The second request waits its turn, sees the slot is now gone, and is turned away politely. On top of that, I added a rule in the database that simply refuses to store the same slot twice — a safety net in case anything ever slips past.
The resultThe two requests get handled one after the other. Whoever is a split second late just sees "this slot was just taken" instead of ending up with a clashing booking. In short: double-bookings can't happen.
02Fast & honest calendar

Keeping the calendar fast — and slots honest while you pay

PatternRead-through caching + time-limited reservation holds (TTL), so reads stay cheap and stale state can't linger
The problemMost people browse lots of time slots before picking one, so the app reads availability far more often than it actually books. Figuring that out from the main database every single time is slow. And a slot you're in the middle of paying for shouldn't still look free to everyone else.
What I didSo I keep a ready-made copy of each day's availability in a fast, in-memory store, and refresh it the moment anyone books. As soon as you start checkout, that slot is "held" just for you and disappears for other people — and if you walk away without paying, the hold quietly expires and the slot comes back.
The resultThe calendar loads in a blink even when lots of people are browsing at once, the main database stays relaxed, and an abandoned checkout frees its slot on its own instead of locking it forever.
03Payments that don't get lost

Trusting the payment provider, not the browser

PatternWebhook-driven state as the source of truth, made safe to retry (idempotency)
The problemRight after paying, a customer might close the tab, lose signal, or get bounced back on a shaky connection. If the app decided "payment done!" based only on the browser coming back, it would miss real payments — or confirm the same one twice when the provider sends its message more than once.
What I didSo instead of trusting the browser, I let the payment provider tell my server directly when money actually goes through. The server double-checks that message is genuine, and is built so that getting the same message twice changes nothing the second time. The page the customer lands on is just a friendly "you're done" screen — not the thing that confirms the booking.
The resultPayments never get lost and never get counted twice, even if the connection drops at the worst possible moment. What you paid for is exactly what you get.
Results & impact
For owners & players
70%Less manual admin for staff
20–30%More revenue from smart pricing
60%of bookings happen on mobile
4.8/5User satisfaction rating
For engineers
90+Lighthouse performance score
100%TypeScript coverage
Tech stack
Next.js 16TypeScriptGolangPostgreSQLRedisMidtransTailwind CSS
Like what you see?
Let's build something solid together.
Start a conversation