CASE STUDY / GO TRANSACTION SYSTEM
Homestay
Active iteration
From inventory calendars to idempotent payments in Go
A WeChat Mini Program homestay-booking MVP. Homestay uses Go, Gin, GORM, MySQL, Redis and WeChat Pay API v3 to connect daily inventory, dynamic pricing, server-side quotes, idempotent orders, inventory locking, order expiration and payment notification confirmation.
01 / BACKEND BRIEF
Why this booking backend deserves a closer look
Homestay booking is more than listing CRUD: inventory changes by stay date, prices can vary night by night, and retries, expiration and payment notifications can modify the same order and stock concurrently. The project keeps MySQL as the source of truth for money and inventory, limits Redis to invalidatable calendar reads, and puts pricing, stock locking and payment confirmation behind explicit server-side transaction boundaries.
- Represent availability and price with a daily inventory model instead of one total room count
- Recalculate quotes on the server and protect concurrent checkout with transactions and row locks
- Use Idempotency-Key and unique constraints to handle retries and repeated submissions
- Connect WeChat Pay preparation, notification verification and payment confirmation to order state
- Use Redis for calendar reads without making it the source of truth for money or inventory
02 / BOOKING FLOW
How one booking moves through inventory and payment
- 01
Discover
The Mini Program loads homestays, room types and a date-range calendar through Gin routes. Calendar reads prefer Redis and fall back to MySQL on misses or cache errors.
- 02
Preview
The client submits dates and room count; Order Service reads daily prices and inventory from the database, calculates the total on the server and checks each night’s availability.
- 03
Create
Checkout requires an Idempotency-Key. Inside a transaction the service looks for an existing idempotent order, locks inventory rows in ascending date order, increments locked_stock and writes the order and night snapshots.
- 04
Expire
A separate worker scans pending orders past their expiry every five seconds, reuses the cancellation path to release locked_stock and invalidates the related calendar cache.
- 05
Pay
In production the payment preparation path calls WeChat Pay API v3 to create a JSAPI prepay order; an explicit mock path is available only in development.
- 06
Confirm
After API v3 signature verification and decryption, the notification enters a confirmation transaction. The service locks the order and inventory, moves locked_stock to sold_stock, creates a unique payment record and returns duplicate notifications idempotently.
03 / ENGINEERING DECISIONS
Backend decisions visible in the code
- 01
Use a modular monolith for the transaction domain
Implementationserver/internal is organised into auth, user, homestay, room, order and payment packages, each with model, repository, service and handler boundaries. The Gin router only composes dependencies and routes.
ValueThe deployment stays simple while booking, inventory and payment rules remain in explicit boundaries that can later be replaced or split without starting from an unstructured CRUD service.
- 02
Keep MySQL as the source of truth for money and stock
ImplementationAmounts use int64 cents, and orders snapshot property, room and nightly prices. Redis stores only versioned room calendars, fails open to MySQL and never becomes the authority for settlement.
ValueThis avoids floating-point money errors and cache-write failures changing inventory, while payment amount checks can compare against persisted order data.
- 03
Model sellable availability per night
Implementationroom_inventory_daily stores total_stock, locked_stock, sold_stock, daily_price and closed for each room type and date. Checkout is exclusive, previews are capped at 93 days and bookings at 30 nights.
ValueVariable prices, closed dates and multi-night reservations can be validated with one server-side model rather than a collection of client-side assumptions.
- 04
Protect checkout with idempotency and a unique index
Implementationorders has a composite unique constraint on user_id and idempotency_key. The service looks up an existing order inside the transaction and, after a concurrent unique-key race, reads and returns the order that won.
ValueNetwork retries, button double-clicks and client timeouts are much less likely to create duplicate orders because the guarantee is shared by application logic and the database.
- 05
Prevent overselling with ordered row locks
ImplementationCreate, cancel and payment confirmation lock inventory rows in ascending date order with SELECT ... FOR UPDATE, check available stock, move locked_stock / sold_stock and verify expected RowsAffected.
ValueConcurrent reservations for the same room type serialize the critical inventory decision, while a fixed lock order reduces deadlock risk across multi-night bookings.
- 06
Invalidate calendars with a versioned cache key
ImplementationCalendar keys include roomTypeID, version, start and end dates. Inventory mutations increment the Redis version; old ranges naturally become unreachable, and cache failures do not block the main transaction.
ValueOrder creation, cancellation and payment confirmation can invalidate every overlapping range without enumerating and deleting every date-range key.
- 07
Separate the payment gateway from local confirmation
ImplementationPayment Service uses a WechatGateway abstraction for production payment creation and notification verification. Local confirmation still validates order amount, merchant identity, AppID and trade status before changing inventory.
ValueThe third-party SDK does not directly mutate business stock, and duplicate callbacks, amount mismatches and expired orders have explicit local decisions.
- 08
Separate migration and runtime roles
ImplementationEmbedded SQL migrations record versions in schema_migrations and run as a separate production role. API and worker are deployed independently, keeping expiration scans out of HTTP request handling.
ValueSchema changes are decoupled from application startup, and background expiry has its own runtime and scaling boundary.
04 / CURRENT BOUNDARIES
Current implementation boundaries and risks
These limitations are directly verifiable in the Go code, deployment configuration and runtime model; they should not be presented as completed production capabilities.
- High priority
Domain services still depend on HTTP response types
Some services return response.Error or net/http semantics directly, so domain rules and transport concerns are not fully separated. Reusing the domain from RPC or asynchronous jobs would first require a unified error mapping.
- High priority
Identity and CORS remain MVP-oriented
JWT uses a custom HS256 implementation without a refresh or revocation path, and CORS currently permits any origin. Production use needs trusted origins, request limiting and a clearer token lifecycle.
- Medium priority
Runtime lifecycle and observability are incomplete
Gin Logger and the standard logger are mixed, with no request ID, metrics or distributed tracing. API and worker context cancellation, readiness, draining and explicit database/Redis closure also need to converge.
- Medium priority
Expiration recovery is polling, not a durable task queue
The worker scans the database on a fixed interval and reuses cancellation logic. This keeps the MVP simple, but there is no lease, retry backoff, failure alert or multi-instance claim semantics.
- Medium priority
The order state model is reserved but only partly driven
The model includes CHECKED_IN, COMPLETED and refund-related states, while the current core path mainly covers pending, confirmed, cancelled and payment notification transitions. State rules still need a single transition boundary.
05 / NEXT ITERATION
What to improve next
- 01
Unify domain errors, request IDs, structured logs, metrics and traces into an observable transaction path
- 02
Use signal.NotifyContext, readiness/draining and cancellable workers, with explicit database and Redis shutdown
- 03
Tighten JWT, CORS and payment-notification security, then add request limiting and sensitive-action auditing
- 04
Add an OpenAPI contract, cache response headers and ETags so frontend and backend evolution does not rely on implicit conventions
- 05
Add retry backoff, failure records and alerting for expiration recovery and payment reconciliation, with a clear manual handling path