An event-driven order processing system in Go, built as a small choreography-style saga on top of a single Kafka topic. It demonstrates Kafka + Redis + PostgreSQL working together on a realistic e-commerce flow: create order → reserve stock → charge payment → ship, with a compensating action when payment fails.
┌─────────────┐
POST /orders ───▶ │ order-api │───▶ PostgreSQL (orders, order_items)
└──────┬──────┘───▶ Redis (idempotency key, status cache)
│ publish OrderCreated
▼
┌──────────────────────┐
│ order-events │ a single Kafka topic,
│ (key = order_id) │ partitioned by order_id
└──────────┬───────────┘ => events for one order are
│ processed in order
┌───────────────────┼───────────────────┬────────────────────┐
▼ ▼ ▼ ▼
┌─────────────┐ ┌───────────────┐ ┌───────────────┐ ┌────────────────┐
│ inventory- │ │ payment- │ │ delivery- │ │ status-updater │
│ service │ │ service │ │ service │ │ │
│ │ │ │ │ │ │ writes audit │
│ OrderCreated│ │InventoryReserv│ │PaymentComplet.│ │ log + updates │
│ -> reserve │ │ -> charge │ │ -> ship │ │ orders.status │
│ stock │ │ (random ~15% │ │ │ │ + Redis cache │
│ │ │ decline) │ │ │ │ (subscribed to │
│PaymentFailed│ │ │ │ │ │ ALL events) │
│ -> release │ │ │ │ │ │ │
│ (compensa- │ │ │ │ │ │ │
│ tion) │ │ │ │ │ │ │
└─────────────┘ └───────────────┘ └───────────────┘ └────────────────┘
Every service is an independent consumer group on the same order-events
topic, each filtering for the event types it cares about. The Kafka message
key is order_id, so all events for a given order land on the same
partition and are processed strictly in order.
CREATED ─▶ INVENTORY_RESERVED ─▶ PAID ─▶ SHIPPED
│ │
└──────┬─────────┘
▼
CANCELLED (out of stock OR payment declined — in the latter case
stock is given back via the compensating
InventoryReleased event)
- Kafka (Redpanda) — the saga's event bus: guarantees event ordering within an order and decouples the services from one another.
- PostgreSQL — the source of truth: orders, products (with atomic
stock reservation via
UPDATE ... WHERE stock >= qty), and an audit log of every saga event (order_events) for debugging/history. - Redis — 1) idempotency for
POST /ordersviaIdempotency-Key(SETNX), 2) a cache of the current order status for fastGET /orders/{id}without hitting Postgres (cache-aside, kept fresh bystatus-updater).
You'll need Go 1.22+ and Docker.
make up # brings up Redpanda (+ Console on :8081), Postgres, Redis
make seed # seeds demo products (one product — prod-webcam — has zero stock)Start the services in four separate terminals (or use tmux/overmind):
make run-api # HTTP :8080
make run-inventory
make run-payment
make run-status
make run-delivery# list products
curl -s localhost:8080/products | jq
# create an order (the idempotency key guards against a double form submit)
curl -s -X POST localhost:8080/orders \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: demo-1' \
-d '{"user_id":"u1","items":[{"product_id":"prod-mouse","qty":2},{"product_id":"prod-keyboard","qty":1}]}'
# => {"order_id":"...","status":"CREATED"}
# check status (the first read may come back CREATED from cache — wait a
# couple of seconds and the saga will reach SHIPPED on its own)
curl -s localhost:8080/orders/<order_id> | jq
# full event history for the order (audit trail)
curl -s localhost:8080/orders/<order_id>/events | jq
# resubmitting with the same Idempotency-Key does not create a second order
curl -s -X POST localhost:8080/orders -H 'Idempotency-Key: demo-1' -d '...'
# a product with zero stock -> the order ends up CANCELLED via InventoryFailed
curl -s -X POST localhost:8080/orders \
-d '{"user_id":"u1","items":[{"product_id":"prod-webcam","qty":1}]}'The payment-service declines payment at random (~15%) — create a few orders
and you'll see both paths: PAID -> SHIPPED and
PaymentFailed -> InventoryReleased -> CANCELLED.
The Redpanda Console at http://localhost:8081 shows raw messages on the
order-events topic — handy for watching the JSON envelopes live.
cmd/api HTTP API: create orders, read status/history/products
cmd/inventory reserve and release stock
cmd/payment simulate payment
cmd/delivery create shipments
cmd/statusupdater single writer of order status + audit log + cache
internal/config env-based configuration
internal/events event envelope and payload types
internal/kafkax producer/consumer wrappers + shared consume loop
internal/db PostgreSQL access (pgx)
internal/cache Redis access (idempotency, status cache)
migrations/ DB schema (applied automatically on Postgres' first start)
scripts/seed.sql demo products
- A handler that returns an error is logged and the message is committed (skipped) rather than retried — production would need a retry/dead-letter topic here.
- No metrics/tracing or API authorization — out of scope for this demo.