A runnable Python microservices example demonstrating an eventually consistent order saga with FastAPI, Kafka, PostgreSQL, Redis, and Docker Compose.
This is an educational POC. It implements several reliability patterns, but it is not a production payment platform.
Client
|
v
API Gateway ---> Order API ---> PostgreSQL (orders + outbox)
| ^
Outbox worker | Order status worker
| |
ORDER_CREATED | payment outcome
v |
Kafka ------------+
|
+--> Payment worker ---> Redis
|
+--> Delivery worker
The Order API, outbox worker, and order status worker are separate processes in the same service boundary. They are the only components that access order data.
| Component | Responsibility | State |
|---|---|---|
| API Gateway | Public API, validation, routing, timeout handling | None |
| Order API | Create and query orders; write outbox events | PostgreSQL |
| Outbox worker | Publish committed order events to Kafka | PostgreSQL |
| Order status worker | Apply payment outcomes to orders | PostgreSQL |
| Payment worker | Simulate a stable payment outcome | Redis |
| Delivery worker | React to successful payments | Logs only |
| Kafka | Transport order and payment events | Kafka log |
- The client submits an order through
POST /place-order. - The Order API atomically stores a
PENDINGorder and an outbox event. - The outbox worker publishes
ORDER_CREATEDand marks the row published. - The Payment worker chooses a simulated success or failure and stores that decision in Redis.
- The Payment worker publishes
PAYMENT_SUCCESSorPAYMENT_FAILED. - The Order status worker changes the order to
COMPLETEDorCANCELLED. - The Delivery worker assigns delivery only after successful payment.
Payment results are intentionally random. Redis ensures that Kafka redelivery reuses the original decision instead of producing a different result.
- Transactional outbox for atomic order and event persistence.
- Manual Kafka offset commits after successful processing.
- Idempotent order updates using PostgreSQL
processed_eventsrecords. - Stable payment decisions keyed by source event ID in Redis.
- Kafka producer acknowledgements and bounded connection retries.
- Versioned event envelopes with UUID and timestamp validation.
orders-dlqandpayments-dlqtopics for contract-invalid events.- Correlation IDs propagated through HTTP, events, and structured logs.
- Gateway timeout and HTTP
503handling for Order API outages. - Database-aware readiness and Compose dependency health checks.
- Non-root application containers.
- Docker Desktop with the Linux container engine running
- Docker Compose v2 (
docker compose) - Python 3.10+ only when running tests outside Docker
Start the complete environment:
docker compose up --buildWait until order-service is healthy, then create an order:
curl -i -X POST http://localhost:8000/place-order \
-H "Content-Type: application/json" \
-H "X-Correlation-ID: demo-order-1" \
-d '{"item":"standard meal"}'The API returns HTTP 202 with a response similar to:
{"order_id":1,"status":"PENDING"}Query the order until the asynchronous saga finishes:
curl -i http://localhost:8000/orders/1The final status will be COMPLETED or CANCELLED.
| Method | Path | Purpose |
|---|---|---|
POST |
/place-order |
Create an order; item length must be 1-200 characters |
GET |
/orders/{order_id} |
Read the current order status |
GET |
/health |
Gateway liveness |
GET |
/ready |
Gateway and Order API readiness |
FastAPI documentation is available at http://localhost:8000/docs.
Domain events use this versioned envelope:
{
"event_id": "UUID",
"event_type": "ORDER_CREATED",
"schema_version": 1,
"occurred_at": "ISO-8601 timestamp",
"correlation_id": "request correlation ID",
"causation_id": null,
"payload": {
"order_id": 1,
"item": "standard meal"
}
}Payment events set causation_id to the source order event ID. Structurally
invalid event objects are wrapped as EVENT_REJECTED and sent to a DLQ before
the source offset is committed.
Follow the complete saga using structured logs:
docker compose logs -f api-gateway order-service order-outbox-worker order-status-worker payment-service delivery-serviceOrder Service exposes these internal endpoints:
/health: verifies PostgreSQL connectivity./metrics: exposes Prometheus metrics, including created order count.
Use the same X-Correlation-ID value to find one request across services.
Run event contract tests locally:
python -m unittest discover -s tests -vValidate the Compose configuration:
docker compose config --quiet| Variable | Used by | Default |
|---|---|---|
ORDER_SERVICE_URL |
API Gateway | http://order-service:8000 |
UPSTREAM_TIMEOUT_SECONDS |
API Gateway | 5 |
DATABASE_URL |
Order processes | PostgreSQL Compose URL |
KAFKA_BOOTSTRAP_SERVERS |
Producers and consumers | kafka:9092 |
KAFKA_CONNECT_ATTEMPTS |
Shared Kafka producer | 5 |
REDIS_URL |
Payment worker | redis://redis:6379/0 |
The credentials and infrastructure ports in docker-compose.yml are local
development defaults. Use managed secrets and private networks outside a POC.
Rebuild the Order processes after changing Order Service code:
docker compose up -d --no-deps --build order-service order-outbox-worker order-status-workerStop services while preserving data:
docker compose downDelete all local PostgreSQL, Redis, and Kafka data:
docker compose down -vThe last command is destructive and is useful when resetting old POC event or database state after a contract change.
Start Docker Desktop and confirm it is using Linux containers, then run:
docker info
docker compose up --buildInspect the outbox and consumer logs:
docker compose logs order-outbox-worker payment-service order-status-workerConfirm Kafka and Redis are running with docker compose ps.
Check orders-dlq or payments-dlq. Malformed JSON that fails before envelope
validation is a known limitation and still requires operational handling.
- Payment state is a Redis decision cache, not an authoritative payment ledger.
- Delivery assignment is logged but not persisted.
- Published outbox rows do not yet have an archival or retention job.
- Retry topics, bounded exponential backoff, and DLQ replay tooling are absent.
- Database schema creation uses SQLAlchemy
create_allinstead of migrations. - Authentication, authorization, rate limiting, tracing, and dashboards are not implemented.
- Kafka, Redis, and PostgreSQL expose development ports and credentials.
See Complete-POC-Doc.md for the architecture review, trade-offs, and prioritized production roadmap.