Dragon Market is an educational marketplace backend written in Go with a REST API, PostgreSQL, Docker Compose, fixed-price orders, timed auctions, wallet reservations, idempotent payments, and concurrency-safe transactions. It is a practical reference for developers looking for a Golang clean architecture example, a PostgreSQL transaction example, or patterns for preventing double-selling in an e-commerce or trading system.
The fantasy-item domain keeps the example approachable: common and rare items use limit orders, while one-of-a-kind legendary items use auctions.
This repository is designed as a compact backend engineering example. Authentication, production observability, and deployment infrastructure are intentionally outside its scope.
- Clean separation between Go domain logic, HTTP transport, and PostgreSQL infrastructure
- Transactional order purchases that prevent concurrent double-selling
- Auction bidding with row locks, a 5% minimum increment, and deadline extension
- Wallet balance reservations, daily spending limits, and an append-only audit trail
- Idempotency keys for safely retrying financial HTTP requests
- Background settlement of expired auctions
- Resilient external price-oracle integration with last-known-good fallback
- PostgreSQL constraints and partial unique indexes for business invariants
- Unit, repository, HTTP, race, and concurrency tests using Testcontainers
- Architecture Decision Records explaining important design choices
| Component | Choice |
|---|---|
| Language | Go 1.25 |
| HTTP router | chi |
| Database | PostgreSQL 16 |
| Database driver | pgx through database/sql |
| Integration testing | Testcontainers for Go |
| Local environment | Docker Compose |
| API client examples | Importable Postman collection |
HTTP request
|
v
internal/api REST handlers and status-code mapping
|
v
internal/{domain} entities, business rules, repository interfaces
|
v
internal/postgres SQL repositories, transactions, row locks
|
v
PostgreSQL
Domain services live in internal/guild, internal/item, internal/order, and internal/auction. They depend on small interfaces rather than database or HTTP packages. PostgreSQL implementations live in internal/postgres; HTTP handlers live in internal/api.
PostgreSQL provides the strong consistency needed for purchases, bids, wallet reservations, and ownership transfers. SELECT ... FOR UPDATE serializes competing operations, while a partial unique index guarantees one active auction per legendary item. See the Architecture Decision Records for the reasoning and tradeoffs.
Requirements: Go 1.25+, Docker, and Docker Compose.
Start PostgreSQL and the included mock price oracle:
docker compose up -d postgres oracleOn a fresh database, Compose runs dev/seed.sql and creates these manual-testing guilds:
| Guild | Gold | Daily limit |
|---|---|---|
guild-seller |
10,000 | 5,000 |
guild-buyer |
10,000 | 5,000 |
guild-bidder |
10,000 | 5,000 |
Run the API:
DATABASE_URL='postgres://market:market@localhost:5432/market?sslmode=disable' \
PRICE_ORACLE_URL='http://localhost:8090' \
go run ./cmd/serverPowerShell:
$env:DATABASE_URL = "postgres://market:market@localhost:5432/market?sslmode=disable"
$env:PRICE_ORACLE_URL = "http://localhost:8090"
go run ./cmd/serverThe API listens on http://localhost:8080. Verify it with:
curl http://localhost:8080/healthzIf the database was initialized before the seed was added, apply it once:
docker compose exec -T postgres psql -U market -d market -f /docker-entrypoint-initdb.d/999_seed.sqlImport Dragon_Market.postman_collection.json into Postman. The collection contains every endpoint, reusable variables, generated idempotency keys, and scripts that capture created item, order, auction, and bid IDs.
Create a guild:
curl -X POST http://localhost:8080/guilds \
-H 'Content-Type: application/json' \
-d '{"id":"guild-demo","gold":10000,"daily_limit":5000}'Create an item:
curl -X POST http://localhost:8080/items \
-H 'Content-Type: application/json' \
-d '{"name":"Arcane Wand","type":"rare","owner_id":"guild-seller","base_price":500}'| Method | Route | Purpose |
|---|---|---|
GET |
/healthz |
Process health check |
POST |
/guilds |
Create a guild and wallet |
GET |
/guilds/{id}/wallet |
Read balance, reservations, and daily spend |
POST |
/items |
Create a common, rare, or legendary item |
GET |
/items |
List free items |
GET |
/items/{id} |
Get an item |
POST |
/orders |
List a common or rare item at a fixed price |
POST |
/orders/{id}/buy |
Buy an order; requires Idempotency-Key |
DELETE |
/orders/{id} |
Cancel an order |
POST |
/auctions |
Start a legendary-item auction |
GET |
/auctions/{id} |
Get an auction |
POST |
/auctions/{id}/bids |
Place a bid; requires Idempotency-Key |
GET |
/bids/{id} |
Get a bid |
DELETE |
/auctions/{auctionID}/bids/{bidID} |
Cancel the current highest bid |
- Purchases lock both the item and order before transferring funds and ownership.
- Auction mutations lock the auction row, producing one serialized bid order.
- Available wallet balance is total gold minus reserved gold.
- Daily spending resets on the next UTC calendar day.
- Wallet mutations append an audit record inside the same transaction.
- Bids reserve funds; an outbid bidder's reservation is released atomically.
- Bids must exceed the current bid by at least 5%.
- Bids placed in the final five minutes extend the auction by five minutes.
- A background worker settles expired auctions; database locks make concurrent workers safe.
- Repeating a completed mutation with the same idempotency key returns its original result.
The external oracle contract is:
GET /prices/{itemID}{"base_price": 1250}Prices refresh immediately at startup and every 30 seconds. Zero, negative, malformed, timed-out, and non-200 responses are rejected. The last valid database value is retained, and one failed item does not stop the rest of the refresh batch. Docker Compose includes a deterministic mock oracle for local development.
cmd/server/ API entry point and dependency wiring
cmd/mock-oracle/ local deterministic price service
internal/api/ HTTP router and handlers
internal/auction/ auction and bidding rules
internal/guild/ wallet and daily-limit rules
internal/item/ item lifecycle
internal/order/ fixed-price order rules
internal/oracle/ HTTP oracle, fallback, and updater
internal/postgres/ PostgreSQL repository implementations
migrations/ ordered schema migrations
docs/adr/ architecture decision records
dev/seed.sql development fixtures
Docker must be running because repository tests start PostgreSQL with Testcontainers.
go build ./...
go test ./...
go test -race ./...
go vet ./...The integration suite covers competing purchases, concurrent bids, duplicate auction creation, repository behavior, and transaction rollback.
- Gold uses an integer smallest unit; floating-point arithmetic is not used.
- Auction deadlines and daily-limit dates use UTC.
- Authentication is outside this example, so guild IDs are treated as trusted input.
- A highest bidder may cancel only while its bid remains active.
- Limit-order prices are locked at listing time and do not change with later oracle updates.
Issues and pull requests are welcome. Read CONTRIBUTING.md for setup, testing, and architectural constraints.
Released under the MIT License. You may use, modify, and redistribute this example with attribution.