Skip to content

Bookstore — example application source

Minimal, real, runnable source for the Bookstore microservices used throughout the guide. The code is deliberately tiny: each service is a vehicle for Kubernetes concepts, not a production e-commerce system. Every Go service is a single static binary in a distroless image; storefront is static files on nginx.

All services run unprivileged and listen on :8080 (override with PORT). The Go services emit structured JSON logs (Go log/slog), expose Prometheus /metrics, and shut down gracefully on SIGTERM.

Services

Service Lang Image tag (used by chapters) Listens Role
catalog Go bookstore/catalog:dev :8080 Book listing API; reads Postgres, caches in Redis
orders Go bookstore/orders:dev :8080 Place orders; writes Postgres, publishes to RabbitMQ
payments-worker Go bookstore/payments-worker:dev :8080 (health/metrics only) Consumes RabbitMQ orders, "processes" payments
storefront static/nginx bookstore/storefront:dev :8080 Browser UI; fetches /api/books, posts /api/orders

postgres, redis, rabbitmq use upstream official images (no source here).

Endpoints

catalog - GET /healthz — liveness, always 200 {"status":"ok"} - GET /readyz — readiness; 503 if a configured DB/cache is unreachable - GET /books — JSON array of books (Postgres if DB_DSN set, else in-memory sample; cached in Redis if REDIS_ADDR set; sets X-Cache: HIT|MISS) - GET /metrics — Prometheus (http_requests_total, http_request_duration_seconds)

orders - POST /orders — body {"book_id":<INT>,"qty":<INT>}; 201 with {order_id,status}; 400 on invalid body - GET /healthz, GET /readyz, GET /metrics (adds orders_placed_total)

payments-worker - GET /healthz, GET /metrics (adds payments_processed_total) - No HTTP business endpoint — it is a queue consumer.

storefront - GET / — the UI; GET /healthz200 {"status":"ok"}

Configuration (environment variables)

Var Services Default Meaning
PORT all 8080 HTTP listen port
LOG_LEVEL go services info debug/info/warn/error (slog)
DB_DSN catalog, orders (unset) Postgres DSN, e.g. postgres://user:pass@postgres:5432/bookstore. Unset → catalog serves sample data; orders log instead of persisting
REDIS_ADDR catalog (unset) Redis host:port. Set → catalog caches the listing (30s TTL)
AMQP_URL orders, payments-worker (unset) RabbitMQ URL, e.g. amqp://guest:guest@rabbitmq:5672/. Unset → orders skip publish; worker idles with a 30s heartbeat

Expected schema when DB_DSN is set (created by the migration Job in a later chapter):

CREATE TABLE books  (id SERIAL PRIMARY KEY, title TEXT, author TEXT, price NUMERIC);
CREATE TABLE orders (id SERIAL PRIMARY KEY, book_id INT, qty INT, created_at TIMESTAMPTZ);

Build the images

Each service builds independently (separate Go modules, multi-stage Dockerfile → gcr.io/distroless/static:nonroot):

docker build -t bookstore/catalog:dev         ./catalog
docker build -t bookstore/orders:dev          ./orders
docker build -t bookstore/payments-worker:dev ./payments-worker
docker build -t bookstore/storefront:dev      ./storefront

(When using kind: kind load docker-image bookstore/catalog:dev etc. With k3d: k3d image import bookstore/catalog:dev.)

Run locally with plain Docker (no Kubernetes)

Zero-dependency mode — no Postgres/Redis/RabbitMQ needed:

docker run --rm -p 8080:8080 bookstore/catalog:dev
curl -s localhost:8080/books        # sample books
curl -s localhost:8080/healthz
curl -s localhost:8080/metrics | head

docker run --rm -p 8081:8080 bookstore/orders:dev
curl -s -X POST localhost:8081/orders \
  -H 'Content-Type: application/json' -d '{"book_id":1,"qty":2}'

docker run --rm -p 8082:8080 bookstore/payments-worker:dev   # idles, heartbeats
docker run --rm -p 8083:8080 bookstore/storefront:dev        # open http://localhost:8083

Develop without Docker

cd catalog && go mod tidy && go vet ./... && go run .

The same applies to orders and payments-worker.