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
Homestay booking architecture showing the WeChat Mini Program, Nginx, Go API, MySQL, Redis, worker and WeChat Pay
Boundaries between the Mini Program, Go API, MySQL source of truth, Redis calendar cache, expiry worker and WeChat Pay.

02 / BOOKING FLOW

How one booking moves through inventory and payment

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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.

  8. 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

  1. 01

    Unify domain errors, request IDs, structured logs, metrics and traces into an observable transaction path

  2. 02

    Use signal.NotifyContext, readiness/draining and cancellable workers, with explicit database and Redis shutdown

  3. 03

    Tighten JWT, CORS and payment-notification security, then add request limiting and sensitive-action auditing

  4. 04

    Add an OpenAPI contract, cache response headers and ETags so frontend and backend evolution does not rely on implicit conventions

  5. 05

    Add retry backoff, failure records and alerting for expiration recovery and payment reconciliation, with a clear manual handling path