11. Part I capstone: Orbit under Compose, fully working
Now complete the three services. These are the final versions; Part IV adds
telemetry with a few lines in main.go.
internal/orders/handler.go: idempotent order creation with an outbox-style
publish.
package orders
import (
"context"
"encoding/json"
"errors"
"log/slog"
"net/http"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/redis/go-redis/v9"
)
const Stream = "orders.created"
type Handler struct {
DB *pgxpool.Pool
RDB *redis.Client
}
type createReq struct {
ProductID int64 `json:"product_id"`
Quantity int `json:"quantity"`
}
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("Idempotency-Key")
if key == "" {
http.Error(w, "Idempotency-Key header required", http.StatusBadRequest)
return
}
var req createReq
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<16)).Decode(&req); err != nil || req.Quantity <= 0 {
http.Error(w, "invalid body", http.StatusBadRequest)
return
}
id, created, err := h.insert(r.Context(), key, req)
if err != nil {
slog.ErrorContext(r.Context(), "create order failed", "err", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
if created {
// Publish after commit. If this fails, a reconciler (or an outbox table) re-publishes
// pending orders; the worker is idempotent, so duplicates are harmless.
if err := h.RDB.XAdd(r.Context(), &redis.XAddArgs{
Stream: Stream, MaxLen: 100_000, Approx: true,
Values: map[string]any{"order_id": id},
}).Err(); err != nil {
slog.WarnContext(r.Context(), "publish failed; will be re-driven", "order_id", id, "err", err)
}
}
w.Header().Set("Content-Type", "application/json")
if created {
w.WriteHeader(http.StatusCreated)
}
_ = json.NewEncoder(w).Encode(map[string]string{"id": id, "status": "pending"})
}
// insert returns the existing order for a repeated idempotency key instead of creating a duplicate.
func (h *Handler) insert(ctx context.Context, key string, req createReq) (string, bool, error) {
id := uuid.NewString()
err := h.DB.QueryRow(ctx,
`INSERT INTO orders (id, idempotency_key, product_id, quantity)
VALUES ($1, $2, $3, $4)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id`, id, key, req.ProductID, req.Quantity).Scan(&id)
if errors.Is(err, pgx.ErrNoRows) { // conflict: already exists
err = h.DB.QueryRow(ctx, `SELECT id FROM orders WHERE idempotency_key=$1`, key).Scan(&id)
return id, false, err
}
return id, err == nil, err
}
internal/worker/worker.go: a consumer group with acknowledgements,
retry via pending-entry reclaim, and graceful stop.
package worker
import (
"context"
"errors"
"log/slog"
"math/rand/v2"
"strings"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/redis/go-redis/v9"
)
type Worker struct {
DB *pgxpool.Pool
RDB *redis.Client
Stream string
Group string
Consumer string // pod name: unique per replica
}
func (w *Worker) Run(ctx context.Context) error {
// Create the group once; BUSYGROUP means it already exists.
if err := w.RDB.XGroupCreateMkStream(ctx, w.Stream, w.Group, "0").Err(); err != nil &&
!strings.HasPrefix(err.Error(), "BUSYGROUP") {
return err
}
for ctx.Err() == nil {
// Reclaim messages another (crashed) consumer held for > 60s: at-least-once delivery.
claimed, _, _ := w.RDB.XAutoClaim(ctx, &redis.XAutoClaimArgs{
Stream: w.Stream, Group: w.Group, Consumer: w.Consumer,
MinIdle: time.Minute, Start: "0-0", Count: 10,
}).Result()
for _, m := range claimed {
w.handle(ctx, m)
}
res, err := w.RDB.XReadGroup(ctx, &redis.XReadGroupArgs{
Group: w.Group, Consumer: w.Consumer, Streams: []string{w.Stream, ">"},
Count: 10, Block: 5 * time.Second,
}).Result()
if errors.Is(err, redis.Nil) || errors.Is(err, context.Canceled) {
continue
}
if err != nil {
slog.Error("xreadgroup", "err", err)
time.Sleep(time.Second)
continue
}
for _, s := range res {
for _, m := range s.Messages {
w.handle(ctx, m)
}
}
}
return nil
}
func (w *Worker) handle(ctx context.Context, m redis.XMessage) {
orderID, _ := m.Values["order_id"].(string)
// Use a context that survives SIGTERM so an in-flight message can finish.
hctx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer cancel()
time.Sleep(time.Duration(50+rand.IntN(150)) * time.Millisecond) // "call the payment provider"
// Idempotent state transition: only pending -> paid.
if _, err := w.DB.Exec(hctx,
`UPDATE orders SET status='paid', updated_at=now() WHERE id=$1 AND status='pending'`, orderID); err != nil {
slog.Error("process order failed; will be retried", "order_id", orderID, "err", err)
return // not ACKed -> reclaimed later by XAUTOCLAIM
}
w.RDB.XAck(hctx, w.Stream, w.Group, m.ID)
slog.Info("order paid", "order_id", orderID)
}
Production notes embedded in this code (each one maps to a later chapter):
- Idempotency keys make client retries safe. Retries happen constantly in Kubernetes: rolling updates, pod evictions, LB timeouts.
- At-least-once + idempotent consumer is the realistic delivery contract. Exactly-once is a property you build, not one you get from the queue.
Consumer= pod name (from the Downward API, §16.4), so reclaimed messages can be traced to the pod that dropped them.- A message that fails forever would loop forever. In production, check
the delivery count (
XPENDING) and move a message to a dead-letter stream after N attempts. Left as an exercise; the pattern is in §43.
The main.go files wire it together. cmd/orders/main.go (the worker's
main is the same shape: it builds a worker.Worker and calls Run(ctx)
under signal.NotifyContext instead of platform.Run):
package main
import (
"context"
"log/slog"
"net/http"
"os"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/redis/go-redis/v9"
"github.com/acme/orbit/internal/orders"
"github.com/acme/orbit/internal/platform"
)
func main() {
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
ctx := context.Background()
pcfg, err := pgxpool.ParseConfig(platform.MustEnv("DATABASE_URL"))
if err != nil {
slog.Error("bad DATABASE_URL", "err", err)
os.Exit(1)
}
pcfg.MaxConns = int32(platform.EnvInt("DB_MAX_CONNS", 10)) // x replicas must stay < Postgres max_connections
db, err := pgxpool.NewWithConfig(ctx, pcfg) // lazy: does not fail if the DB is down at boot
if err != nil {
slog.Error("db pool", "err", err)
os.Exit(1)
}
defer db.Close()
rdb := redis.NewClient(&redis.Options{Addr: platform.MustEnv("REDIS_ADDR")})
defer rdb.Close()
health := platform.NewHealth(
platform.Check{Name: "postgres", Fn: db.Ping},
platform.Check{Name: "redis", Fn: func(ctx context.Context) error { return rdb.Ping(ctx).Err() }},
)
h := &orders.Handler{DB: db, RDB: rdb}
mux := http.NewServeMux()
mux.HandleFunc("GET /livez", health.Livez)
mux.HandleFunc("GET /readyz", health.Readyz)
mux.HandleFunc("POST /orders", h.Create)
if err := platform.Run(platform.ServerConfig{
Addr: ":" + platform.Env("PORT", "8080"),
DrainDelay: platform.EnvDuration("DRAIN_DELAY", 5*time.Second),
ShutdownTimeout: platform.EnvDuration("SHUTDOWN_TIMEOUT", 20*time.Second),
}, mux, health); err != nil {
slog.Error("server failed", "err", err)
os.Exit(1)
}
}
Note the boot behavior: the process starts even if Postgres is down (the pool connects lazily), stays alive, and reports not ready until the dependency is reachable. That is exactly what Kubernetes probes expect. A service that crashes on boot when its DB is unavailable produces crash-loop storms during every database failover.
Run it:
docker compose --profile obs up -d --build --wait
curl -s localhost:8081/products | jq
curl -s -XPOST localhost:8082/orders -H 'Idempotency-Key: demo-1' \
-H 'Content-Type: application/json' -d '{"product_id":1,"quantity":2}' | jq
docker compose exec db psql -U orbit -c 'select id,status from orders' # pending -> paid within ~1s
Part I checkpoint: a 10 MB distroless, non-root, read-only image per
service; a reproducible local stack with healthchecks and migrations; and
you can explain what happens on docker stop.
⚠️ Caveat (verify): the Go code in §4.1 and §11 was written for this guide and has not been compiled. Assemble the repo and run
go mod tidy && go build ./... && go vet ./...before trusting it. Known spots to check: the return values of go-redis v9XAutoClaim, and theuuiddependency (go get github.com/google/uuid).